Skip to content

Commit e05848d

Browse files
committed
feat: add train cancel rpc and ui
1 parent 0f1e950 commit e05848d

18 files changed

Lines changed: 332 additions & 40 deletions

cppmega_v4/jsonrpc/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
JsonRpcError,
2929
JsonRpcRequest,
3030
JsonRpcResponse,
31+
PipelineAbortParams,
32+
PipelineAbortResult,
3133
ProbeRunParams,
3234
ProbeRunResult,
3335
SuggestAdaptersParams,
@@ -53,6 +55,8 @@
5355
"JsonRpcResponse",
5456
"LRUCache",
5557
"METHOD_REGISTRY",
58+
"PipelineAbortParams",
59+
"PipelineAbortResult",
5660
"ProbeRunParams",
5761
"ProbeRunResult",
5862
"SCHEMA_VERSION",

cppmega_v4/jsonrpc/dispatcher.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
JsonRpcError,
5151
JsonRpcRequest,
5252
JsonRpcResponse,
53+
PipelineAbortParams,
54+
PipelineAbortResult,
5355
PipelineRunParams,
5456
PipelineRunResult,
5557
ProbeRunParams,
@@ -90,6 +92,10 @@
9092
PipelineRunParams,
9193
lambda p, c: _pipeline_run(p),
9294
),
95+
"pipeline.abort": (
96+
PipelineAbortParams,
97+
lambda p, c: _pipeline_abort(p),
98+
),
9399
"tokenizer.encode_visualize": (
94100
EncodeVisualizeParams,
95101
lambda p, c: encode_visualize(p, cache=c),
@@ -134,6 +140,12 @@ def _pipeline_run(params: PipelineRunParams) -> PipelineRunResult:
134140
return PipelineRunResult.model_validate(report.to_dict())
135141

136142

143+
def _pipeline_abort(params: PipelineAbortParams) -> PipelineAbortResult:
144+
from cppmega_v4.runner.stages import request_abort
145+
request_abort(params.run_id)
146+
return PipelineAbortResult(run_id=params.run_id)
147+
148+
137149
def dispatch(
138150
payload: Mapping[str, Any] | JsonRpcRequest,
139151
*,

cppmega_v4/jsonrpc/schema.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -663,13 +663,30 @@ class PipelineRunParams(BaseModel):
663663
pipeline: PipelinePayload
664664

665665

666+
class PipelineAbortParams(BaseModel):
667+
"""Request cancellation of an in-flight pipeline train stage."""
668+
669+
model_config = ConfigDict(extra="forbid")
670+
671+
run_id: str = Field(min_length=1)
672+
673+
674+
class PipelineAbortResult(BaseModel):
675+
"""Acknowledgement that the train abort token has been set."""
676+
677+
model_config = ConfigDict(extra="forbid")
678+
679+
status: Literal["abort_requested"] = "abort_requested"
680+
run_id: str
681+
682+
666683
class StageResult(BaseModel):
667684
"""One stage execution result."""
668685

669686
model_config = ConfigDict(extra="allow")
670687

671688
name: str
672-
status: Literal["ok", "skipped", "fail"]
689+
status: Literal["ok", "skipped", "fail", "cancelled"]
673690
elapsed_ms: float
674691
warnings: int = 0
675692
errors: int = 0
@@ -682,7 +699,7 @@ class PipelineRunResult(BaseModel):
682699
model_config = ConfigDict(extra="forbid")
683700

684701
stages: list[StageResult]
685-
overall_status: Literal["ok", "fail"]
702+
overall_status: Literal["ok", "fail", "cancelled"]
686703
total_elapsed_ms: float
687704

688705

@@ -707,6 +724,7 @@ class PipelineRunResult(BaseModel):
707724
"backend.status",
708725
"probe.run",
709726
"pipeline.run",
727+
"pipeline.abort",
710728
})
711729

712730

@@ -717,6 +735,7 @@ class PipelineRunResult(BaseModel):
717735
"build_preset_specs",
718736
"probe.run",
719737
"pipeline.run",
738+
"pipeline.abort",
720739
"backend.status",
721740
"tokenizer.encode_visualize",
722741
"tokenizer.list_presets",

cppmega_v4/jsonrpc/server.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def cache_clear() -> dict[str, str]:
7373

7474
@app.post("/rpc")
7575
async def rpc(payload: dict) -> dict:
76-
response = dispatch(payload, cache=cache)
76+
response = await _dispatch(payload, cache)
7777
return response.model_dump(mode="json", exclude_none=True)
7878

7979
@app.websocket("/ws")
@@ -92,7 +92,7 @@ async def ws(socket: WebSocket) -> None:
9292
"data": {"detail": str(exc)}},
9393
})
9494
continue
95-
response = dispatch(payload, cache=cache)
95+
response = await _dispatch(payload, cache)
9696
await socket.send_json(
9797
response.model_dump(mode="json", exclude_none=True)
9898
)
@@ -108,6 +108,12 @@ async def ws(socket: WebSocket) -> None:
108108
return app
109109

110110

111+
async def _dispatch(payload: dict, cache: LRUCache):
112+
if payload.get("method") == "pipeline.run":
113+
return await asyncio.to_thread(dispatch, payload, cache=cache)
114+
return dispatch(payload, cache=cache)
115+
116+
111117
async def _heartbeat(socket: WebSocket) -> None:
112118
while True:
113119
await asyncio.sleep(_HEARTBEAT_INTERVAL_S)

cppmega_v4/runner/pipeline.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ def run_pipeline(spec: VerifyParams, pipeline: Pipeline) -> PipelineReport:
9090
stage = STAGE_REGISTRY[name]
9191
result = stage(ctx)
9292
results.append(result)
93-
if result.status == "fail" and not pipeline.continue_on_failure:
93+
if (
94+
result.status in {"fail", "cancelled"}
95+
and not pipeline.continue_on_failure
96+
):
9497
# Mark every subsequent stage as skipped for visibility.
9598
for remaining in pipeline.stages[len(results):]:
9699
results.append(StageResult(
@@ -100,6 +103,8 @@ def run_pipeline(spec: VerifyParams, pipeline: Pipeline) -> PipelineReport:
100103
overall = "ok"
101104
if any(r.status == "fail" for r in results):
102105
overall = "fail"
106+
elif any(r.status == "cancelled" for r in results):
107+
overall = "cancelled"
103108
elapsed = (time.perf_counter() - t0) * 1000.0
104109
return PipelineReport(
105110
stages=results, overall_status=overall, total_elapsed_ms=elapsed,

cppmega_v4/runner/stages.py

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from cppmega_v4.jsonrpc.schema import VerifyParams
4343

4444

45-
StageStatus = Literal["ok", "skipped", "fail"]
45+
StageStatus = Literal["ok", "skipped", "fail", "cancelled"]
4646

4747

4848
@dataclass
@@ -119,6 +119,36 @@ def _fail(name: str, t0: float, exc: BaseException) -> StageResult:
119119
)
120120

121121

122+
def _cancelled_train_result(
123+
t0: float,
124+
*,
125+
abort_token: str,
126+
step: int,
127+
losses: list[float],
128+
lr_trajectory: list[float],
129+
schedule_kind_label: str,
130+
optimizer_kind: str,
131+
weight_delta_norm: float = 0.0,
132+
) -> StageResult:
133+
clear_abort(abort_token)
134+
return StageResult(
135+
name="train", status="cancelled",
136+
elapsed_ms=(time.perf_counter() - t0) * 1000.0,
137+
extras={
138+
"losses": [round(loss_item, 4) for loss_item in losses],
139+
"lr_trajectory": [
140+
round(lr_item, 6) for lr_item in lr_trajectory
141+
],
142+
"weight_delta_norm": round(weight_delta_norm, 6),
143+
"num_steps": step,
144+
"schedule_kind": schedule_kind_label,
145+
"optimizer_kind": optimizer_kind,
146+
"aborted": True,
147+
"abort_token": abort_token,
148+
},
149+
)
150+
151+
122152
def stage_parse(ctx: StageContext) -> StageResult:
123153
"""Materialise loss/optim/graph from the wire-form spec."""
124154
t0 = time.perf_counter()
@@ -726,7 +756,14 @@ def loss_fn(model: nn.Module, emb: mx.array, tgt: mx.array) -> mx.array:
726756
token_count = len(tokens)
727757
except Exception:
728758
pass
759+
train_token_embedding = None
760+
train_input_source = "random"
761+
if data_source != "synthetic":
762+
train_token_embedding = nn.Embedding(vocab_size, hidden)
763+
train_input_source = "token_embedding"
729764
all_modules = nn.Sequential(*modules, *lm_heads)
765+
if train_token_embedding is not None:
766+
all_modules.train_token_embedding = train_token_embedding
730767
if side_channel_token_embedding is not None:
731768
all_modules.side_channel_token_embedding = side_channel_token_embedding
732769
opt, optimizer_kind = _build_optimizer(spec_optim, lr)
@@ -939,21 +976,14 @@ def _count(tree: Any) -> int:
939976
for step in range(n_steps):
940977
if abort_token is not None and abort_token in _ABORT_TOKENS:
941978
# Stop early; return partial extras with cancellation flag.
942-
return StageResult(
943-
name="train", status="ok",
944-
elapsed_ms=(time.perf_counter() - t0) * 1000.0,
945-
extras={
946-
"losses": [round(loss_item, 4)
947-
for loss_item in losses],
948-
"lr_trajectory": [round(lr_item, 6)
949-
for lr_item in lr_trajectory],
950-
"weight_delta_norm": 0.0,
951-
"num_steps": step,
952-
"schedule_kind": schedule_kind_label,
953-
"optimizer_kind": optimizer_kind,
954-
"aborted": True,
955-
"abort_token": abort_token,
956-
},
979+
return _cancelled_train_result(
980+
t0,
981+
abort_token=str(abort_token),
982+
step=step,
983+
losses=losses,
984+
lr_trajectory=lr_trajectory,
985+
schedule_kind_label=schedule_kind_label,
986+
optimizer_kind=optimizer_kind,
957987
)
958988
# If a schedule callable exists, override optimizer's
959989
# learning_rate per step. MLX optimizers accept a fresh
@@ -965,8 +995,11 @@ def _count(tree: Any) -> int:
965995
else:
966996
lr_trajectory.append(lr)
967997

968-
emb = mx.random.normal(shape=(batch, seq, hidden), key=rng_key)
969-
rng_key, _ = mx.random.split(rng_key)
998+
if train_token_embedding is None:
999+
emb = mx.random.normal(shape=(batch, seq, hidden), key=rng_key)
1000+
rng_key, _ = mx.random.split(rng_key)
1001+
else:
1002+
emb = train_token_embedding(targets)
9701003
loss, grads = loss_and_grad(all_modules, emb, targets)
9711004
mx.eval(loss, grads)
9721005
if probe_key is None:
@@ -1002,6 +1035,25 @@ def _count(tree: Any) -> int:
10021035
opt.update(all_modules, grads)
10031036
mx.eval(all_modules.parameters(), opt.state)
10041037
losses.append(float(loss.item()))
1038+
if abort_token is not None and abort_token in _ABORT_TOKENS:
1039+
partial_delta = 0.0
1040+
if probe_key is not None and probe_before is not None:
1041+
after_flat = dict(
1042+
nn.utils.tree_flatten(all_modules.parameters()))
1043+
partial_delta = float(
1044+
mx.linalg.norm(
1045+
after_flat[probe_key] - probe_before).item()
1046+
)
1047+
return _cancelled_train_result(
1048+
t0,
1049+
abort_token=str(abort_token),
1050+
step=step + 1,
1051+
losses=losses,
1052+
lr_trajectory=lr_trajectory,
1053+
schedule_kind_label=schedule_kind_label,
1054+
optimizer_kind=optimizer_kind,
1055+
weight_delta_norm=partial_delta,
1056+
)
10051057

10061058
try:
10071059
if hasattr(mx, "metal"):
@@ -1110,6 +1162,7 @@ def _count(tree: Any) -> int:
11101162
"schedule_kind": schedule_kind_label,
11111163
"optimizer_kind": optimizer_kind,
11121164
"data_source": data_source,
1165+
"train_input_source": train_input_source,
11131166
"token_count": token_count,
11141167
"tokenizer_used": tokenizer_used,
11151168
"loss_kind": (

tests/v4/test_jsonrpc_dispatcher.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
LRUCache,
99
dispatch,
1010
)
11+
from cppmega_v4.runner.stages import clear_abort
1112

1213

1314
_GRAPH = {
@@ -105,3 +106,41 @@ def test_dispatch_pipeline_run_round_trip():
105106
assert resp.error is None
106107
assert resp.result["overall_status"] == "ok"
107108
assert len(resp.result["stages"]) == 2
109+
110+
111+
def test_dispatch_pipeline_abort_requests_stage_abort():
112+
run_id = "dispatcher-abort-1"
113+
clear_abort(run_id)
114+
try:
115+
resp = dispatch({
116+
"jsonrpc": "2.0", "id": "abort", "method": "pipeline.abort",
117+
"params": {"run_id": run_id},
118+
})
119+
assert resp.error is None
120+
assert resp.result == {
121+
"status": "abort_requested",
122+
"run_id": run_id,
123+
}
124+
125+
envelope = {
126+
"jsonrpc": "2.0", "id": "train", "method": "pipeline.run",
127+
"params": {
128+
"spec": {"graph": _GRAPH, "dim_env": _DIM_ENV,
129+
"loss": _LOSS, "optim": _OPTIM},
130+
"pipeline": {
131+
"stages": ["parse", "verify_build_spec", "build_model",
132+
"train"],
133+
"stage_options": {
134+
"train": {"num_steps": 8, "abort_token": run_id},
135+
},
136+
},
137+
},
138+
}
139+
run_resp = dispatch(envelope)
140+
assert run_resp.error is None
141+
assert run_resp.result["overall_status"] == "cancelled"
142+
train = run_resp.result["stages"][-1]
143+
assert train["status"] == "cancelled"
144+
assert train["aborted"] is True
145+
finally:
146+
clear_abort(run_id)

0 commit comments

Comments
 (0)