Skip to content

Commit 99ae963

Browse files
authored
Merge pull request #959 from blugassi/training-report-profiling
feat: extend training report with profiling and step aggregation
2 parents 93a8a58 + e849b67 commit 99ae963

6 files changed

Lines changed: 417 additions & 46 deletions

File tree

src/cloudai/models/workload.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ def __hash__(self) -> int:
8989
return self.git_repo.__hash__()
9090

9191

92+
class TrainingReportConfig(BaseModel):
93+
"""Training-report aggregation window: steps excluded before computing per-metric stats."""
94+
95+
model_config = ConfigDict(extra="forbid")
96+
97+
exclude_start_steps: int = Field(default=5, ge=0)
98+
exclude_post_profiling_steps: int = Field(default=2, ge=0)
99+
100+
92101
class TestDefinition(BaseModel, ABC):
93102
"""Base Test object."""
94103

@@ -106,6 +115,7 @@ class TestDefinition(BaseModel, ABC):
106115
git_repos: list[GitRepo] = []
107116
nsys: Optional[NsysConfiguration] = None
108117
predictor: Optional[PredictorConfig] = None
118+
training_report: Optional[TrainingReportConfig] = None
109119

110120
agent: str = "grid_search"
111121
agent_steps: int = 1

src/cloudai/report_generator/training/mappings.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@
5252
}
5353

5454

55-
# (world_size, num_nodes, model_name) and the computed data_parallel_size are not mapped here.
56-
NEMO_CONFIG: dict[str, str] = {
55+
# Framework's resolved config artifact. (world_size, num_nodes, model_name) and computed data_parallel_size are not
56+
# mapped here.
57+
NEMO_MODEL_CONFIG: dict[str, str] = {
5758
"micro_batch_size": "data.micro_batch_size",
5859
"global_batch_size": "data.global_batch_size",
5960
"seq_length": "data.seq_length",
@@ -77,7 +78,7 @@
7778
"moe_grouped_gemm": "model.moe_grouped_gemm",
7879
}
7980

80-
MEGATRON_CONFIG: dict[str, str] = {
81+
MEGATRON_MODEL_CONFIG: dict[str, str] = {
8182
"micro_batch_size": "micro_batch_size",
8283
"global_batch_size": "global_batch_size",
8384
"seq_length": "seq_length",
@@ -101,7 +102,7 @@
101102
"moe_grouped_gemm": "moe_grouped_gemm",
102103
}
103104

104-
MEGATRON_BRIDGE_CONFIG: dict[str, str] = {
105+
MEGATRON_BRIDGE_MODEL_CONFIG: dict[str, str] = {
105106
"micro_batch_size": "train.micro_batch_size",
106107
"global_batch_size": "train.global_batch_size",
107108
"seq_length": "model.seq_length",
@@ -124,3 +125,29 @@
124125
"moe_ffn_hidden_size": "model.moe_ffn_hidden_size",
125126
"moe_grouped_gemm": "model.moe_grouped_gemm",
126127
}
128+
129+
130+
# CloudAI TestDefinition (user TOML + defaults). TrainingConfig field -> dotted path in TestDefinition.model_dump().
131+
NEMO_TEST_CONFIG: dict[str, str] = {
132+
"profiling_enabled": "nsys.enable",
133+
"profiling_start_step": "extra_cmd_args.*start_step",
134+
"profiling_stop_step": "extra_cmd_args.*end_step",
135+
"exclude_start_steps": "training_report.exclude_start_steps",
136+
"exclude_post_profiling_steps": "training_report.exclude_post_profiling_steps",
137+
}
138+
139+
MEGATRON_TEST_CONFIG: dict[str, str] = {
140+
"profiling_enabled": "nsys.enable",
141+
"profiling_start_step": "cmd_args.profile_step_start",
142+
"profiling_stop_step": "cmd_args.profile_step_end",
143+
"exclude_start_steps": "training_report.exclude_start_steps",
144+
"exclude_post_profiling_steps": "training_report.exclude_post_profiling_steps",
145+
}
146+
147+
MEGATRON_BRIDGE_TEST_CONFIG: dict[str, str] = {
148+
"profiling_enabled": "cmd_args.enable_nsys",
149+
"profiling_start_step": "cmd_args.profiling_start_step",
150+
"profiling_stop_step": "cmd_args.profiling_stop_step",
151+
"exclude_start_steps": "training_report.exclude_start_steps",
152+
"exclude_post_profiling_steps": "training_report.exclude_post_profiling_steps",
153+
}

src/cloudai/report_generator/training/models.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,43 @@
1616

1717
"""Data models for training parsers."""
1818

19+
import statistics
1920
from collections.abc import Hashable
2021
from dataclasses import MISSING, dataclass, fields
2122
from typing import Any, List, Optional
2223

2324

25+
@dataclass
26+
class MetricStats:
27+
"""Aggregated statistics for one metric over the filtered steps."""
28+
29+
mean: float
30+
min: float
31+
max: float
32+
std: float
33+
t99: float
34+
t95: float
35+
36+
@classmethod
37+
def from_values(cls, values: list[float]) -> "MetricStats":
38+
"""Build stats from a non-empty list of values (population std; inclusive percentiles)."""
39+
return cls(
40+
mean=statistics.mean(values),
41+
min=min(values),
42+
max=max(values),
43+
std=statistics.pstdev(values),
44+
t99=cls._percentile(values, 99),
45+
t95=cls._percentile(values, 95),
46+
)
47+
48+
@staticmethod
49+
def _percentile(values: list[float], p: int) -> float:
50+
"""Inclusive, linearly-interpolated p-th percentile; returns the sole value for a single sample."""
51+
if len(values) == 1:
52+
return float(values[0])
53+
return statistics.quantiles(values, n=100, method="inclusive")[p - 1]
54+
55+
2456
@dataclass(frozen=True)
2557
class Scalar:
2658
"""A single scalar event from a training run (source-agnostic: TensorBoard today, others later)."""
@@ -51,6 +83,29 @@ class TrainingStep:
5183
OPTIONAL_STEP_FIELDS = {f.name for f in fields(TrainingStep) if f.default is not MISSING}
5284

5385

86+
@dataclass(kw_only=True)
87+
class StepAggregation:
88+
"""Per-metric aggregated statistics over the filtered steps."""
89+
90+
step_time_sec: MetricStats
91+
loss: MetricStats
92+
memory_reserved_bytes: MetricStats
93+
memory_allocated_bytes: MetricStats
94+
tflops_per_gpu: Optional[MetricStats] = None
95+
96+
@classmethod
97+
def from_steps(cls, steps: list["TrainingStep"]) -> "StepAggregation":
98+
"""Build per-metric stats from a non-empty list of already-filtered steps."""
99+
tflops = [s.tflops_per_gpu for s in steps if s.tflops_per_gpu is not None]
100+
return cls(
101+
step_time_sec=MetricStats.from_values([s.step_time_sec for s in steps]),
102+
loss=MetricStats.from_values([s.loss for s in steps]),
103+
memory_reserved_bytes=MetricStats.from_values([s.memory_reserved_bytes for s in steps]),
104+
memory_allocated_bytes=MetricStats.from_values([s.memory_allocated_bytes for s in steps]),
105+
tflops_per_gpu=MetricStats.from_values(tflops) if tflops else None,
106+
)
107+
108+
54109
@dataclass(kw_only=True)
55110
class TrainingConfig:
56111
"""
@@ -95,6 +150,15 @@ class TrainingConfig:
95150
world_size: Optional[int] = None # CloudAI-computed (None when gpus_per_node is unavailable)
96151
num_nodes: int = 0 # CloudAI-computed
97152

153+
# Profiling (CloudAI-computed from the run's nsys/profiler settings)
154+
profiling_enabled: bool = False
155+
profiling_start_step: Optional[int] = None
156+
profiling_stop_step: Optional[int] = None
157+
158+
# Aggregation window (steps dropped before computing the top-level aggregation)
159+
exclude_start_steps: int = 5
160+
exclude_post_profiling_steps: int = 2
161+
98162
# Identity
99163
test_template_name: str = "" # CloudAI-computed
100164

@@ -105,3 +169,4 @@ class TrainingResults:
105169

106170
config: TrainingConfig
107171
steps: List[TrainingStep]
172+
aggregation: Optional[StepAggregation] = None # None when no steps remain after exclusions

0 commit comments

Comments
 (0)