Skip to content

Commit fd1e86f

Browse files
committed
feat(v7-h06): stage_train wait_while_paused + pipeline.pause/resume RPCs (3/3 pytest)
1 parent 677d4ec commit fd1e86f

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""V7-H06: stage_train wait_while_paused integration + RPC handlers."""
2+
3+
from __future__ import annotations
4+
5+
import threading
6+
import time
7+
8+
import pytest
9+
10+
from cppmega_v4.jsonrpc.dispatcher import (
11+
_pipeline_pause, _pipeline_resume,
12+
)
13+
from cppmega_v4.jsonrpc.schema import (
14+
PipelineAbortParams, VerifyParams,
15+
)
16+
from cppmega_v4.runner import Pipeline, run_pipeline
17+
from cppmega_v4.runtime import job_control as jc
18+
19+
20+
@pytest.fixture(autouse=True)
21+
def _reset():
22+
jc.reset()
23+
yield
24+
jc.reset()
25+
26+
27+
def _spec() -> VerifyParams:
28+
return VerifyParams.model_validate({
29+
"graph": {
30+
"nodes": [
31+
{"id": "attn", "kind": "attention",
32+
"params": {"num_heads": 4, "head_dim": 64}},
33+
{"id": "mlp", "kind": "mlp", "params": {}},
34+
],
35+
"edges": [{"src": "attn", "dst": "mlp"}],
36+
},
37+
"dim_env": {"B": 1, "S": 8, "H": 128,
38+
"nh": 2, "nkv": 1, "head_dim": 64},
39+
"loss": {"kind": "cross_entropy", "head_outputs": ["mlp"]},
40+
"optim": {"kind": "adamw",
41+
"groups": [{"matcher": "all", "lr": 1e-3,
42+
"weight_decay": 0.01,
43+
"betas": [0.9, 0.95]}]},
44+
})
45+
46+
47+
def test_v7_h06_pipeline_pause_rpc_sets_flag():
48+
r = _pipeline_pause(PipelineAbortParams(run_id="abc"))
49+
assert r.run_id == "abc"
50+
assert jc.is_paused("abc") is True
51+
52+
53+
def test_v7_h06_pipeline_resume_rpc_clears_flag():
54+
jc.pause("xyz")
55+
r = _pipeline_resume(PipelineAbortParams(run_id="xyz"))
56+
assert r.run_id == "xyz"
57+
assert jc.is_paused("xyz") is False
58+
59+
60+
def test_v7_h06_stage_train_waits_while_paused_then_completes():
61+
"""Pause job-T mid-loop from another thread, resume after 200ms,
62+
confirm train finishes with the expected step count."""
63+
jc.pause("job-T")
64+
65+
def _resumer():
66+
time.sleep(0.2)
67+
jc.resume("job-T")
68+
69+
threading.Thread(target=_resumer, daemon=True).start()
70+
t0 = time.time()
71+
rep = run_pipeline(_spec(), Pipeline.from_dict({
72+
"stages": ["parse", "verify_build_spec", "build_model", "train"],
73+
"stage_options": {"train": {
74+
"num_steps": 2, "abort_token": "job-T",
75+
}},
76+
}))
77+
elapsed = time.time() - t0
78+
tr = next(s for s in rep.stages if s.name == "train")
79+
assert tr.status == "ok", f"paused train failed: {tr.error}"
80+
assert len(tr.extras["losses"]) == 2
81+
# Total wall-clock includes the 0.2s pause delay.
82+
assert elapsed > 0.15

0 commit comments

Comments
 (0)