Skip to content

Commit d26dfb9

Browse files
committed
feat(v7-h10): abort_token honoured by verify_build_spec + dry_forward
Closes item 19 of the UI<->API wiring honesty list. Previously only stage_train respected _ABORT_TOKENS, so a UI cancel during a long verify on a big graph had zero effect until train started. - run_pipeline picks up any abort_token declared in stage_options (typically train.abort_token) and gates every stage entry. - New _stage_abort_token helper resolves the same token from inside individual stages. - stage_verify_build_spec and stage_dry_forward gate at entry and return status='cancelled' instead of running to completion. Honest scope: cancellation is at stage boundaries — MLX itself can't be interrupted mid-call, so verify_build_spec on a partially-finished graph still completes the current stage's work before yielding. UI now has at-most-one-stage of latency per cancel click. bd: cppmega-mlx-lmor 4/4 pytest + 14/14 lifecycle regression.
1 parent b4edf60 commit d26dfb9

3 files changed

Lines changed: 182 additions & 0 deletions

File tree

cppmega_v4/runner/pipeline.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,41 @@ def run_pipeline(spec: VerifyParams, pipeline: Pipeline) -> PipelineReport:
8282
"""Run ``pipeline`` over ``spec`` and return the report.
8383
8484
Stops on the first failed stage unless ``continue_on_failure`` is set.
85+
86+
V7-H10: between stages, check if the pipeline-wide abort_token (any
87+
stage that declares one in its stage_options) has been signalled via
88+
pipeline.abort RPC. If so, mark all remaining stages as cancelled —
89+
this makes verify_build_spec / dry_forward etc. cooperatively
90+
cancellable at stage boundaries even though they can't be
91+
interrupted mid-call.
8592
"""
93+
from cppmega_v4.runner.stages import _ABORT_TOKENS, clear_abort
94+
95+
def _pipeline_abort_token() -> str | None:
96+
"""Pick the first abort_token declared in any stage's options."""
97+
for _name, _opts in (pipeline.stage_options or {}).items():
98+
tok = (_opts or {}).get("abort_token")
99+
if tok:
100+
return str(tok)
101+
return None
102+
86103
t0 = time.perf_counter()
87104
ctx = StageContext(spec=spec, options=pipeline.stage_options)
88105
results: list[StageResult] = []
106+
abort_token = _pipeline_abort_token()
89107
for name in pipeline.stages:
108+
# V7-H10: cancellation gate before each stage.
109+
if abort_token is not None and abort_token in _ABORT_TOKENS:
110+
results.append(StageResult(
111+
name=name, status="cancelled", elapsed_ms=0.0,
112+
error={"type": "Aborted", "abort_token": abort_token},
113+
))
114+
for remaining in pipeline.stages[len(results):]:
115+
results.append(StageResult(
116+
name=remaining, status="skipped", elapsed_ms=0.0,
117+
))
118+
clear_abort(abort_token)
119+
break
90120
stage = STAGE_REGISTRY[name]
91121
result = stage(ctx)
92122
results.append(result)

cppmega_v4/runner/stages.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,22 @@ def _cancelled_train_result(
159159
)
160160

161161

162+
def _stage_abort_token(ctx: StageContext, stage_name: str) -> str | None:
163+
"""V7-H10: resolve the pipeline-wide abort token for a stage entry.
164+
165+
Prefer the per-stage opts override, fall back to any abort_token
166+
declared by any other stage in the pipeline (typically train) so a
167+
single UI cancel propagates to verify / dry_forward / etc."""
168+
own = ctx.opts(stage_name).get("abort_token")
169+
if own:
170+
return str(own)
171+
for _opts in (ctx.options or {}).values():
172+
tok = (_opts or {}).get("abort_token")
173+
if tok:
174+
return str(tok)
175+
return None
176+
177+
162178
def stage_parse(ctx: StageContext) -> StageResult:
163179
"""Materialise loss/optim/graph from the wire-form spec."""
164180
t0 = time.perf_counter()
@@ -184,6 +200,17 @@ def stage_parse(ctx: StageContext) -> StageResult:
184200

185201
def stage_verify_build_spec(ctx: StageContext) -> StageResult:
186202
t0 = time.perf_counter()
203+
# V7-H10: respect pipeline-level abort_token at stage entry so a
204+
# cancel issued during a long graph verify still produces a
205+
# cancelled result instead of forcing the user to wait it out.
206+
_tok = _stage_abort_token(ctx, "verify_build_spec")
207+
if _tok and _tok in _ABORT_TOKENS:
208+
clear_abort(_tok)
209+
return StageResult(
210+
name="verify_build_spec", status="cancelled",
211+
elapsed_ms=(time.perf_counter() - t0) * 1000.0,
212+
error={"type": "Aborted", "abort_token": _tok},
213+
)
187214
try:
188215
if ctx.build_spec is None:
189216
return _fail("verify_build_spec", t0,
@@ -331,6 +358,15 @@ def stage_build_model(ctx: StageContext) -> StageResult:
331358

332359
def stage_dry_forward(ctx: StageContext) -> StageResult:
333360
t0 = time.perf_counter()
361+
# V7-H10: same cancel gate as stage_verify_build_spec.
362+
_tok = _stage_abort_token(ctx, "dry_forward")
363+
if _tok and _tok in _ABORT_TOKENS:
364+
clear_abort(_tok)
365+
return StageResult(
366+
name="dry_forward", status="cancelled",
367+
elapsed_ms=(time.perf_counter() - t0) * 1000.0,
368+
error={"type": "Aborted", "abort_token": _tok},
369+
)
334370
try:
335371
opts = ctx.opts("dry_forward")
336372
seq = int(opts.get("S", 8))
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""V7-H10: abort_token honoured by verify_build_spec + dry_forward.
2+
3+
Previously only stage_train respected _ABORT_TOKENS. UI clicking cancel
4+
during a long verify on a big graph had no effect — the user had to
5+
wait it out. This pins:
6+
7+
- stage_verify_build_spec returns 'cancelled' at entry if aborted.
8+
- stage_dry_forward returns 'cancelled' at entry if aborted.
9+
- run_pipeline driver, between stages, halts and marks remaining
10+
stages 'skipped' if abort_token is set.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import pytest
16+
17+
from cppmega_v4.jsonrpc.dispatcher import _pipeline_abort
18+
from cppmega_v4.jsonrpc.schema import PipelineAbortParams, VerifyParams
19+
from cppmega_v4.runner import Pipeline, run_pipeline
20+
from cppmega_v4.runner.stages import _ABORT_TOKENS, request_abort
21+
from cppmega_v4.runtime import run_registry as rr
22+
23+
24+
@pytest.fixture(autouse=True)
25+
def _reset():
26+
_ABORT_TOKENS.clear()
27+
rr.reset()
28+
yield
29+
_ABORT_TOKENS.clear()
30+
rr.reset()
31+
32+
33+
def _spec() -> VerifyParams:
34+
return VerifyParams.model_validate({
35+
"graph": {
36+
"nodes": [
37+
{"id": "attn", "kind": "attention",
38+
"params": {"num_heads": 4, "head_dim": 64}},
39+
{"id": "mlp", "kind": "mlp", "params": {}},
40+
],
41+
"edges": [{"src": "attn", "dst": "mlp"}],
42+
},
43+
"dim_env": {"B": 1, "S": 8, "H": 128,
44+
"nh": 2, "nkv": 1, "head_dim": 64},
45+
"loss": {"kind": "cross_entropy", "head_outputs": ["mlp"]},
46+
"optim": {"kind": "adamw",
47+
"groups": [{"matcher": "all", "lr": 1e-3,
48+
"weight_decay": 0.01,
49+
"betas": [0.9, 0.95]}]},
50+
})
51+
52+
53+
def test_v7_h10_verify_build_spec_stage_function_cancels_at_entry():
54+
"""Direct stage call: the entry-gate inside stage_verify_build_spec
55+
fires when its abort_token is in _ABORT_TOKENS, independent of the
56+
pipeline driver."""
57+
from cppmega_v4.runner.stages import (
58+
StageContext, stage_verify_build_spec, stage_parse,
59+
)
60+
ctx = StageContext(spec=_spec(),
61+
options={"verify_build_spec":
62+
{"abort_token": "ABRT-V"}})
63+
parse_result = stage_parse(ctx)
64+
assert parse_result.status == "ok"
65+
66+
request_abort("ABRT-V")
67+
v = stage_verify_build_spec(ctx)
68+
assert v.status == "cancelled"
69+
assert v.error == {"type": "Aborted", "abort_token": "ABRT-V"}
70+
71+
72+
def test_v7_h10_dry_forward_stage_function_cancels_at_entry():
73+
"""Direct stage call: stage_dry_forward's entry-gate fires."""
74+
from cppmega_v4.runner.stages import (
75+
StageContext, stage_dry_forward, stage_parse, stage_build_model,
76+
stage_verify_build_spec, stage_resolve_shapes,
77+
stage_estimate_memory,
78+
)
79+
ctx = StageContext(spec=_spec(),
80+
options={"dry_forward":
81+
{"abort_token": "ABRT-DF"}})
82+
assert stage_parse(ctx).status == "ok"
83+
assert stage_verify_build_spec(ctx).status == "ok"
84+
assert stage_resolve_shapes(ctx).status == "ok"
85+
assert stage_estimate_memory(ctx).status == "ok"
86+
assert stage_build_model(ctx).status == "ok"
87+
88+
request_abort("ABRT-DF")
89+
df = stage_dry_forward(ctx)
90+
assert df.status == "cancelled"
91+
assert df.error == {"type": "Aborted", "abort_token": "ABRT-DF"}
92+
93+
94+
def test_v7_h10_pipeline_driver_between_stages_halts_on_abort_rpc():
95+
"""Run a pipeline whose train stage carries an abort_token; trigger
96+
pipeline.abort RPC up-front so the very first between-stage gate
97+
fires before parse even runs."""
98+
_pipeline_abort(PipelineAbortParams(run_id="ABRT-PD"))
99+
rep = run_pipeline(_spec(), Pipeline.from_dict({
100+
"stages": ["parse", "verify_build_spec", "build_model", "train"],
101+
"stage_options": {"train": {"num_steps": 2,
102+
"abort_token": "ABRT-PD"}},
103+
}))
104+
parse = next(s for s in rep.stages if s.name == "parse")
105+
assert parse.status == "cancelled"
106+
assert rep.overall_status == "cancelled"
107+
108+
109+
def test_v7_h10_no_abort_token_still_runs_to_completion():
110+
rep = run_pipeline(_spec(), Pipeline.from_dict({
111+
"stages": ["parse", "verify_build_spec", "build_model", "train"],
112+
"stage_options": {"train": {"num_steps": 2}},
113+
}))
114+
assert rep.overall_status == "ok"
115+
for s in rep.stages:
116+
assert s.status == "ok", f"{s.name}: {s.status} {s.error}"

0 commit comments

Comments
 (0)