Skip to content

Commit 6eb5be1

Browse files
committed
Integrate current Puzzletron v2 base
Apply the reviewed PR 2125 base integration, corrections, and review feedback as one fast-forward commit. Signed-off-by: Johannes Rausch <jrausch@nvidia.com>
1 parent b9e6f92 commit 6eb5be1

40 files changed

Lines changed: 2861 additions & 657 deletions

examples/puzzletron/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,8 +461,7 @@ python examples/puzzletron/main.py \
461461
--gpus-per-node 8
462462
```
463463

464-
The authoritative dependencies and enabled-stage rules live in
465-
[`stages/graph.py`](../../modelopt/torch/puzzletron/stages/graph.py).
464+
The dependency-free [`StageSpec` registry](../../modelopt/torch/puzzletron/stages/graph.py) is the authoritative contract for every public stage's identity, dependencies, enablement, semantic config sections, and static completion artifacts. Add or change those properties there. Default execution strategies remain scheduler-specific and live in the [orchestration compiler](../../modelopt/torch/puzzletron/orchestration/compiler.py); handlers, scheduler adapters, mesh resolution, and heavyweight artifact validators remain separate runtime concerns.
466465

467466
| Stage | Purpose |
468467
|---|---|

examples/puzzletron/embedding_pipeline.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,9 @@ def run_embedding_stage(
272272
),
273273
check=True,
274274
)
275+
# Workers validate their scenario-local runtime statistics before
276+
# building a replacement library, so seed them from the root aggregate.
277+
_project_vllm_stats_to_scenarios(config)
275278
_run_commands(
276279
scenario_worker_commands(
277280
config_path=config_path,

examples/puzzletron/main.py

Lines changed: 82 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,21 @@
4545
semantic_stage_config,
4646
write_stage_manifest,
4747
)
48+
from modelopt.torch.puzzletron.orchestration.adapters.stage_compat import (
49+
stage_is_complete as artifacts_are_complete,
50+
)
51+
from modelopt.torch.puzzletron.orchestration.adapters.stage_compat import (
52+
stage_output_patterns as canonical_stage_output_patterns,
53+
)
4854
from modelopt.torch.puzzletron.stages.graph import (
55+
StageStatus,
4956
configured_parent_stage_ids,
5057
distributed_stage_ids,
5158
enabled_stage_ids,
5259
required_stage_ids,
5360
stage_ids,
5461
stage_is_enabled,
62+
stage_terminal_state,
5563
topological_stage_ids,
5664
)
5765

@@ -64,30 +72,6 @@
6472
PIPELINE_STAGE_ORDER = topological_stage_ids()
6573
REQUIRED_STAGES = frozenset(required_stage_ids())
6674
DISTRIBUTED_STAGES = frozenset(distributed_stage_ids())
67-
REQUIRED_OUTPUT_PATTERNS = {
68-
"convert": ("ckpts/teacher/config.json",),
69-
"tokenize_data": ("dataset_cache/*.tokens", "dataset_cache/*.tokens.json"),
70-
"sort": ("ckpts/sorted_teacher/config.json",),
71-
"slicing_sanity": (
72-
"artifacts/width_slice_equivalence/manifest.json",
73-
"artifacts/width_slice_equivalence/summary.json",
74-
"artifacts/width_slice_equivalence/cases/**/*.json",
75-
"artifacts/width_slice_equivalence/comparisons/*.safetensors",
76-
),
77-
"depth_importance": ("depth/iterative/trajectory.json",),
78-
"build_library": (
79-
"replacement_library.json",
80-
"candidate_library.json",
81-
"subblock_stats.json",
82-
),
83-
"vllm_stats": ("artifacts/vllm_stats/summary.json",),
84-
"replacement_scoring": ("artifacts/replacement_scoring/summary.json",),
85-
"mip": ("mip/**/*.json",),
86-
"zero_shot_evaluation": ("artifacts/**/evaluation_summary.json",),
87-
"aiperf": ("artifacts/aiperf/**/*.json",),
88-
"global_distillation_sanity": ("artifacts/global_distillation_sanity/**/*.json",),
89-
"global_distillation": ("artifacts/global_distillation/**/*.json",),
90-
}
9175

9276

9377
def _register_faulthandler() -> None:
@@ -216,50 +200,17 @@ def _report_model_name(config: dict) -> str:
216200
)
217201

218202

219-
def _manifest_is_complete(config: dict, stage: str) -> bool:
203+
def _manifest_terminal_state(config: dict, stage: str):
220204
puzzle_dir = Path(config.get("puzzle_dir") or (config.get("experiment") or {})["dir"])
221205
path = puzzle_dir / "manifests" / f"{stage}.json"
222206
try:
223207
payload = json.loads(path.read_text())
224208
except (OSError, ValueError):
225-
return False
226-
return payload.get("status") in {"success", "imported"}
227-
228-
229-
def _runtime_stats_filename(config: dict) -> str:
230-
stats = config.get("vllm_stats") or {}
231-
return str(stats.get("subblock_stats_filename", "subblock_stats.json"))
232-
233-
234-
def _stage_output_patterns(config: dict, stage: str) -> tuple[str, ...]:
235-
if stage == "vllm_stats":
236-
return (_runtime_stats_filename(config),)
237-
if stage == "slicing_sanity":
238-
slicing_cfg = config.get("slicing_sanity") or {}
239-
if slicing_cfg.get("backend") == "distributed_parent_sweep":
240-
return ("artifacts/slicing_sanity/summary.json",)
241-
patterns = REQUIRED_OUTPUT_PATTERNS.get(stage, ())
242-
if stage == "build_library":
243-
resolved = [
244-
_runtime_stats_filename(config) if pattern == "subblock_stats.json" else pattern
245-
for pattern in patterns
246-
]
247-
embedding = config.get("embedding_pruning") or {}
248-
if bool(embedding.get("enabled", False)):
249-
resolved.append("scenarios/width_scenarios.json")
250-
for configured_width in embedding.get("widths", ()):
251-
scenario = f"scenarios/width-{int(configured_width):04d}/depth-00"
252-
resolved.extend(
253-
(
254-
f"{scenario}/scenario_manifest.json",
255-
f"{scenario}/replacement_library.json",
256-
f"{scenario}/candidate_library.json",
257-
f"{scenario}/{_runtime_stats_filename(config)}",
258-
f"{scenario}/manifests/build_library.json",
259-
)
260-
)
261-
return tuple(resolved)
262-
return patterns
209+
return None
210+
state = stage_terminal_state(payload, expected_stage=stage)
211+
if state is None or not state.allows_completion(stage, config):
212+
return None
213+
return state
263214

264215

265216
def _resume_kwargs(config: dict, config_path: str | Path, stage: str) -> dict:
@@ -280,7 +231,7 @@ def _resume_kwargs(config: dict, config_path: str | Path, stage: str) -> dict:
280231
"depth": None,
281232
"required_patterns": (
282233
f"manifests/{stage}.json",
283-
*_stage_output_patterns(config, stage),
234+
*canonical_stage_output_patterns(config, stage),
284235
),
285236
"upstream_markers": upstream,
286237
"stage_config": semantic_stage_config(config, stage),
@@ -289,19 +240,76 @@ def _resume_kwargs(config: dict, config_path: str | Path, stage: str) -> dict:
289240

290241

291242
def _completion_is_valid(config: dict, config_path: str | Path, stage: str) -> bool:
292-
if not _manifest_is_complete(config, stage):
243+
state = _manifest_terminal_state(config, stage)
244+
if state is None:
245+
return False
246+
if state.status is StageStatus.SKIPPED:
247+
return True
248+
if not artifacts_are_complete(config, stage):
293249
return False
294250
kwargs = _resume_kwargs(config, config_path, stage)
295251
return check_marker(marker_path(kwargs["root"], stage, None, None), **kwargs)
296252

297253

298254
def _mark_completion(config: dict, config_path: str | Path, stage: str) -> None:
299-
if not _manifest_is_complete(config, stage):
300-
raise RuntimeError(f"enabled stage {stage!r} did not write a successful manifest")
255+
state = _manifest_terminal_state(config, stage)
256+
if state is None:
257+
raise RuntimeError(f"stage {stage!r} did not write an accepted terminal manifest")
258+
if state.status is StageStatus.SKIPPED:
259+
return
260+
if not artifacts_are_complete(config, stage):
261+
raise RuntimeError(f"stage {stage!r} failed canonical artifact validation")
301262
kwargs = _resume_kwargs(config, config_path, stage)
302263
write_marker(kwargs["root"], stage, build_payload(**kwargs))
303264

304265

266+
def _validate_worker_result(config: dict, result, *, expected_stage: str | None = None) -> None:
267+
"""Fail the worker unless its result, manifest, and required artifacts agree."""
268+
269+
expected_stage = expected_stage or result.stage
270+
if result.stage != expected_stage:
271+
raise RuntimeError(
272+
f"worker stage {expected_stage!r} returned result for stage {result.stage!r}"
273+
)
274+
puzzle_dir = Path(config.get("puzzle_dir") or (config.get("experiment") or {})["dir"])
275+
expected_manifest_path = puzzle_dir / "manifests" / f"{expected_stage}.json"
276+
if Path(result.manifest_path).resolve() != expected_manifest_path.resolve():
277+
raise RuntimeError(
278+
f"stage {expected_stage!r} returned manifest path {result.manifest_path!s}; "
279+
f"expected {expected_manifest_path!s}"
280+
)
281+
try:
282+
payload = json.loads(expected_manifest_path.read_text())
283+
except (OSError, ValueError) as exc:
284+
raise RuntimeError(f"stage {expected_stage!r} wrote an unreadable manifest") from exc
285+
if payload.get("stage") != expected_stage:
286+
raise RuntimeError(
287+
f"stage {expected_stage!r} manifest identifies stage {payload.get('stage')!r}"
288+
)
289+
state = stage_terminal_state(payload, expected_stage=expected_stage)
290+
if state is None or not state.allows_completion(expected_stage, config):
291+
raise RuntimeError(f"stage {expected_stage!r} wrote an invalid terminal manifest")
292+
if result.status != state.status.value:
293+
raise RuntimeError(
294+
f"stage {expected_stage!r} result status {result.status!r} disagrees with "
295+
f"manifest status {state.status.value!r}"
296+
)
297+
expected_reason = state.skip_reason.value if state.skip_reason is not None else None
298+
if result.skip_reason != expected_reason:
299+
raise RuntimeError(
300+
f"stage {expected_stage!r} result skip reason {result.skip_reason!r} disagrees with "
301+
f"manifest skip reason {expected_reason!r}"
302+
)
303+
if not state.produced_artifacts:
304+
return
305+
if not artifacts_are_complete(config, expected_stage):
306+
expected = canonical_stage_output_patterns(config, expected_stage)
307+
raise RuntimeError(
308+
f"stage {expected_stage!r} failed canonical artifact validation; expected: "
309+
+ (", ".join(expected) or "stage-specific outputs")
310+
)
311+
312+
305313
def run_pipeline(
306314
*,
307315
config_path: str | Path,
@@ -369,7 +377,9 @@ def _run_worker(args: argparse.Namespace) -> None:
369377
)
370378
gpus_per_node = int(args.gpus_per_node or (cfg.get("execution") or {}).get("gpus_per_node", 8))
371379
composite_only = {"replacement_scoring", "mip"}
372-
if args.worker_stage == "tokenize_data":
380+
if not _stage_enabled(cfg, args.worker_stage):
381+
result = mtpz.stage_runner.run_stage(cfg, args.worker_stage, handlers={})
382+
elif args.worker_stage == "tokenize_data":
373383
if __package__:
374384
from .tokenize_data import tokenize_data_stage
375385
else:
@@ -405,13 +415,15 @@ def _run_worker(args: argparse.Namespace) -> None:
405415
)
406416
outputs["base_manifest"] = str(result.manifest_path)
407417
result = _complete_composite_stage(cfg, args.worker_stage, outputs)
418+
if int(os.environ.get("RANK", "0")) == 0:
419+
if result.status == "failed":
420+
refresh_campaign_report(cfg)
421+
_validate_worker_result(cfg, result, expected_stage=args.worker_stage)
408422
refresh_campaign_report(cfg)
409423
mtpz.tools.mprint(
410424
f"Puzzletron stage {result.stage!r} finished with status {result.status}: "
411425
f"{result.manifest_path}"
412426
)
413-
if result.status not in {"success", "skipped"}:
414-
raise RuntimeError(f"stage {result.stage!r} finished with status {result.status!r}")
415427

416428

417429
def _complete_composite_stage(config: dict, stage: str, outputs: dict):

examples/puzzletron/tokenize_data.py

Lines changed: 16 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,79 +13,43 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
"""Tokenize the fixed data caches consumed by Puzzletron stages."""
16+
"""Optionally materialize fixed-token caches ahead of Puzzletron stages."""
1717

1818
from __future__ import annotations
1919

2020
import subprocess
2121
import sys
2222
from pathlib import Path
23-
from typing import TYPE_CHECKING, Any
24-
25-
if TYPE_CHECKING:
26-
from collections.abc import Mapping
2723

2824
from modelopt.torch.puzzletron.manifest import StageManifest, write_stage_manifest
2925
from modelopt.torch.puzzletron.stage_runner import StageResult
26+
from modelopt.torch.puzzletron.stages.graph import StageSkipReason, stage_is_enabled
27+
from puzzletron_orchestrator.token_caches import resolve_tokenize_caches
3028

3129
__all__ = ["resolve_tokenize_caches", "tokenize_data_stage"]
3230

3331

34-
def resolve_tokenize_caches(config: Mapping[str, Any]) -> list[dict[str, Any]]:
35-
"""Return explicit tokenize caches, or defaults from campaign token paths."""
36-
37-
stage_config = config.get("tokenize_data") or {}
38-
caches = [dict(cache) for cache in stage_config.get("caches") or ()]
39-
if caches:
40-
return caches
41-
42-
data_cfg = config.get("data") or {}
43-
calibration = data_cfg.get("calibration") or {}
44-
train_samples = int(calibration.get("num_samples") or 32768)
45-
train_seq = int(calibration.get("seq_len") or data_cfg.get("max_sample_length") or 4096)
46-
scoring = data_cfg.get("replacement_scoring") or {}
47-
val_samples = int(scoring.get("num_samples") or 128)
48-
configured_seed = (config.get("pruning") or {}).get("shuffle_seed")
49-
train_seed = 444 if configured_seed is None else int(configured_seed)
50-
51-
defaults: list[dict[str, Any]] = []
52-
train_path = config.get("train_token_cache_path")
53-
if train_path:
54-
defaults.append(
55-
{
56-
"output": str(train_path),
57-
"split": "train",
58-
"num_samples": train_samples,
59-
"seq_length": train_seq,
60-
"shuffle_seed": train_seed,
61-
}
62-
)
63-
validation_path = config.get("validation_token_cache_path")
64-
if validation_path:
65-
defaults.append(
66-
{
67-
"output": str(validation_path),
68-
"split": "validation",
69-
"num_samples": val_samples,
70-
"seq_length": train_seq,
71-
"shuffle_seed": train_seed + 1,
72-
}
73-
)
74-
return defaults
75-
76-
7732
def tokenize_data_stage(config: dict) -> StageResult:
78-
"""Build every configured cache and record a normal stage manifest."""
33+
"""Materialize every configured fixed-token cache ahead of its consumers."""
7934

8035
stage_config = config.get("tokenize_data") or {}
8136
puzzle_dir = Path(config.get("puzzle_dir") or (config.get("experiment") or {})["dir"])
8237
manifest_path = puzzle_dir / "manifests" / "tokenize_data.json"
8338
manifest = StageManifest(stage="tokenize_data", inputs={"config": config}, config=config)
84-
if not bool(stage_config.get("enabled", False)):
85-
manifest.complete(outputs={"enabled": False}, status="skipped")
39+
if not stage_is_enabled("tokenize_data", config):
40+
skip_reason = StageSkipReason.DISABLED
41+
manifest.complete(
42+
outputs={"enabled": False},
43+
status="skipped",
44+
skip_reason=skip_reason,
45+
)
8646
write_stage_manifest(manifest_path, manifest)
8747
return StageResult(
88-
"tokenize_data", "skipped", manifest_path, "Data tokenization is disabled."
48+
"tokenize_data",
49+
"skipped",
50+
manifest_path,
51+
"Ahead-of-time fixed-token cache materialization is disabled.",
52+
skip_reason.value,
8953
)
9054

9155
caches = resolve_tokenize_caches(config)

0 commit comments

Comments
 (0)