Repository navigation
Phase 8.2 WP1: freeze the worker protocol and add the Python decode-baseline workers - #87
Merged
Merged
Conversation
Two digest-pinned workers under tools/decode-baseline/ implementing the frozen adapter contracts (reference findings section 6): - controlled.py: the synchronous controlled loop (single full-prompt forward, last-position own-head projection, exactly 127 cached steps, no lookahead/async/cache-clear, evaluated logits and cache state at every step boundary, first-maximum greedy tie-break) plus the fixed-continuation stepper with per-step argmax and top-2 margin. - worker.py: the job-consuming entry point (schema jmlx-decode-baseline-job-1): full pre-load validation (manifest and file digests, venv pins, installed generate.py digest, canonical prompt digests, run_policy agreement, output-dir ownership), per-request priming (findings 7.3), headline/fixed-continuation/ reference-pair modes for both adapters, the worker-ready handshake marker, a 1s-poll watchdog with 30s grace backstop (failed report plus exit 3, never a partial success), and the jmlx-decode-baseline-1 report written atomically exactly once. Verified on this host against the pinned venv and 0.6b artifact: 26 full-policy requests per adapter (128 tokens / 127 steps for headline, 32 fed steps for fixed-continuation), zero argmax mismatches between the two adapters on the fixed continuation, and the stock unused_lookahead_forwards/drain contract.
- workloads.json: add the run_policy block (the frozen section 7.4 finite limits, exact nonpositive-rejecting values) and the worker_sources digest pin for the two committed Python workers; new frozen digest 772753ac... - build.gradle: verifyDecodeBaselineManifest gains structural checks for both blocks (frozen values, cache_regime, io_affected_cells, emitted_tokens cross-check; exact worker file set with path confinement and per-file SHA-256 re-hash) and the two worker files as task inputs; the digest backstop moves to the new value. Negative tampers: run-policy-limit, run-policy-regime, io-affected-cells, worker-source-digest, worker-file-edit and worker-file-set each fail with a specific error; restore verified byte-identical. - findings: new section 13 freezes the launcher/worker job schema, output layout, report record shapes and boundary semantics (including the section 10 amendments), records the verified WP1 facts (stop-set resolution correction, adapter-level greedy divergence vs the logit-level identity claim, fixed-continuation argmax agreement, Metal first-use warm-up, mlx API facts) and the worker re-pin flow; section 2/2.3 citations and the guard description updated to the new digest and checks.
…manifest Addresses all seven findings and all four suggestions from the review of head 4ef0503 (see req/plans/phase8-2-reference-findings.md section 13.4 items 9-12 for the contract changes and evidence): - a refused job can no longer destroy an existing report (hard link instead of replace; refusal on stderr only) - the fired deadline is re-checked after the final request - the artifact safetensors set must equal the pinned set exactly - run_policy.first_requests: 1 frozen in manifest, guard and worker - the full 202-file installed mlx_lm source freeze is re-hashed per run - manifest_path is pinned to the canonical repository file - fixed-continuation step timers and full_generation_s are defined identically in both adapters; controlled token 1 event time taken after its argmax; the worker directory is appended to sys.path - the report write is lock-guarded and the watchdog backstop skips the status overwrite once the report is written The round-1 timeout probe caught and fixed a watchdog rewrite regression (fired-event set unreachable, so no timeout could ever fire) and verified both the in-loop and post-loop timeout paths. Manifest re-pinned (new digest 23271da6...), guard backstop updated, guard tampers re-executed with byte-identical restore, full smoke re-run green.
…e manifest Addresses all three findings from the review of head a2f580d (see req/plans/phase8-2-reference-findings.md section 13.4 item 13 for the contract changes and evidence): - repo_dir is now required to resolve to the repository the worker runs from (anchored on the worker file, like manifest_path), so the report's code identity can never name a different checkout - fixed-continuation full_generation_s is the step-region wall time (prefill excluded) in both adapters, and the stock prompt cache is created before the timer starts; prefill_s is documented per-adapter and not cross-adapter comparable (the stock shipped call projects the output head at every prompt position, the controlled one only at the last) - report writing: per-process temp name; a success whose report link is refused is downgraded to failed (exit 1); a temp-write failure removes the temp file before propagating Manifest re-pinned (digest 1c691642...), guard backstop updated, guard tampers re-executed with byte-identical restore, worker negative suites (13 + 14 assertions) and the T2 timeout probe (in-loop 22-record break and post-loop 26-record timeout on the final head) re-run green, full smoke re-run green.
…e manifest Addresses both findings from the review of head 602016e (see req/plans/phase8-2-reference-findings.md section 13.4 item 14 for the contract changes and evidence): - the worker claims the launcher-created output directory with an exclusive-create worker-claim file ({utc, pid}) before any validation; on an existing claim it writes nothing into the directory, reports the refusal on stderr and exits 1 (a second worker -- valid or not -- can no longer take over a folder owned by another worker; the claim is sticky: a claimed folder without a report is dead for the launcher) - the report's worker block carries the worker's pid, worker-ready is created with exclusive create, and the report-link refusal message is built from the final state and printed after the success downgrade, so it never reports an ok status the run was downgraded away from - fixed-continuation's full_generation_s is renamed step_region_s (with full_generation_s null in that mode, and step_region_s null in the other modes -- uniform key set, one value per mode), since the step region includes the per-step full-vocabulary sort/argmax host work (about 39-46% of the 0.6b L=512 value), not decode time; decode comparisons use the per-step s values Manifest re-pinned (digest 10eaaa0c...), guard backstop updated, guard tampers re-executed with byte-identical restore, worker negative suites (13 + 14 assertions), the new round-3 suite (26 assertions, including the reviewer's repro), the T2 timeout probe (11/11; the probe's aim itself was fixed -- the deadline sits at ooff + T, not T, because the worker sets it before the mlx_lm import/load, and the post-loop branch is now exercised on the reference-pair single-request cell) and the full smoke re-run (30 cross-checks) all green.
…e manifest Addresses both findings from the review of head 94fea72 (see req/plans/phase8-2-reference-findings.md section 13.4 item 15 for the contract changes and evidence): - an out_dir that is not an existing directory is now rejected before the claim create with the old message ("worker validation failed: out_dir must be created by the launcher", exit 1, nothing written), instead of surfacing as a raw FileNotFoundError from the claim; the now-dead out_dir check in validate is removed, and the module docstring's claim bullet records the rejection - the two comments still calling the step region "the comparable quantity" (controlled.py and the stock fixed-continuation branch in worker.py) are reworded to agree with the updated section 13.3 (the step region is not decode time; decode comparisons use the per-step s values), and the controlled.py docstring/local variable are renamed full_s to step_region_s to match the report field (the controlled.py rewording rides this round's re-pin) Manifest re-pinned (digest e8c77301...), guard backstop updated, guard tampers re-executed with byte-identical restore, the worker negative suites (20 + 14 assertions, including new T8 missing-dir and T9 file-in-the-way tests), the round-3 suite (26 assertions) and the T2 timeout probe (11/11: in-loop break at T=8 s on the first attempt under the corrected T-space aim, post-loop check at T=1 s on the reference-pair cell) and the full smoke re-run (30 cross-checks) all green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 8.2 WP1 (plan
req/plans/phase8-2-plan.md), part 1 of 2: the frozenworker protocol and the Python side of the benchmark workers. The Java side
(launcher, statistics/validation,
jmlx-modelstest-source adapters,tiny-fixture gate tests, CI required-suite list) lands on this branch before
the review cycle.
What this PR contains
Frozen contract — findings §13 (
req/plans/phase8-2-reference-findings.md)jmlx-decode-baseline-job-1job schema (launcher → worker) with everyvalidation rule; worker protocol (launcher-created unique
out_dir,worker-readyhandshake marker, atomic single-writereport.json,exit codes 0/1/3, 1 s-poll watchdog with 30 s grace backstop,
overwrite rejection, per-request priming).
jmlx-decode-baseline-1record shapes: per-worker report with theidentity block moved out of the session (the launcher-assembled session
report embeds worker reports and keeps host/fork-order/native-pin
fields), per-request
event_s/tokens, fixed-continuationargmax_id/top2_marginsteps, and the explicit §10 amendments.mlx_lm.load()mergesgeneration_config.json's eos into the wrapper(all three text cases already resolve to exactly the manifest stop sets;
the worker still completes + asserts defensively), and controlled-vs-
stock greedy streams are not a cross-adapter invariant (divergence at
token 103 on the frozen random 0.6b prompt, margin 0.125; the §2.4
logit-level identity claim is unaffected) — fixed-continuation argmax
agreement holds with zero mismatches over 26 × 32 steps per adapter.
Python workers (
tools/decode-baseline/worker.py,controlled.py)implementing the frozen §6 adapter contracts: the controlled synchronous
loop (single full-prompt forward, last-position own-head projection,
exactly 127 cached steps, no lookahead/async/cache-clear, evaluated state
at every step boundary) and the stock path (shipped
stream_generateunmodified, stop-set completion via the shipped
add_eos_tokenAPI withequality assertion, explicit
mx.synchronize()drain with the unusedlookahead forward counted). Headline, fixed-continuation and
reference-pair modes;
MLX_ENABLE_TF32set before any mlx import;venv pins and the full installed
mlx_lmsource freeze (all 202 pinnedfiles) re-verified per run.
Manifest re-pin + guard extension
workloads.jsongainsrun_policy(the frozen §7.4 finite limits,including
first_requests: 1) andworker_sources(SHA-256 of bothworkers, path-confined); new frozen digest
23271da6...(round-1re-pin), cited in §2 and pinned by the guard.
verifyDecodeBaselineManifest(rootcheck) gains structural checksfor both blocks plus the two worker files as task inputs. Negative
tampers executed: run-policy-limit, run-policy-regime,
io-affected-cells, worker-source-digest, worker-file-edit and
worker-file-set each fail with a specific error; restore verified
byte-identical (§2.3).
Evidence (this host, M5 Max — contract verification, no headline measurement)
pinned 0.6b artifact: headline 128 tokens / 127 steps (positions
L+1…L+127), stock
unused_lookahead_forwards=1with finishlength;fixed-continuation L=512: 32 fed steps, positions 513–544, zero argmax
mismatches between the two adapters including exact-tie steps.
out_dirleaves a failedreport + exit 1;
verify.shstillVERIFIED; root./gradlew checkgreen;
git diff --checkclean.Deliberately not in this PR
jmlx-modelsadapters and tiny-fixture gate tests — next commit(s) onthis branch, implementing §13.
execution in ordinary
checkbeyond the pure-Java guard, andbenchmarkDecode/benchmarkBatchDecode/benchmarkTf32untouched.Review round 1 (commit 3, head of this branch)
All seven findings and all four suggestions from the review of head
4ef0503are addressed:write_reportnow hard-links the temp file (
FileExistsErrorinstead of replace), sono path — validation failure, watchdog backstop or otherwise — can
replace an existing
report.json; the refusal (with the worker's reason)goes to stderr only. Verified: a pre-seeded report survives a refused job
byte-identical (exit 1).
ok. The fired deadline isre-checked after the final request. Verified by a temporary real-manifest
tamper (full 26-request mix, 0.6b L=512): deadline inside the final
request →
status: timeout, exit 3, 26 complete records; deadline insiderequest 25 → in-loop break, 25 records. Manifest restored byte-identical.
*.safetensorsset must equal the case's pinned set exactly; a straymodel-extra.safetensorsin a cloned artifact dir is now rejected.paths stop the step timer at the evaluated logits (argmax + full-vocab
sort outside the interval), and
full_generation_sis the monotonic walltime from entry in both.
after it, like every later token.
requests.firstunchecked.run_policy.first_requests: 1isfrozen (manifest + guard + worker, outside reference-pair mode) and added
to the identity block; a
first: 2job is rejected.write_reportislock-guarded; the backstop skips the status overwrite once the report is
written (the exit stays unconditional).
manifest_pathmust resolve to thecanonical repository
workloads.json(a matching copy is rejected); thefull 202-file installed
mlx_lmsource freeze is re-hashed per run (notjust
generate.py); the worker directory is appended tosys.path(not prepended);
positionis documented as "1-based position of the fedtoken — the cache length after the step" (findings §13.3,
controlled.py).The round-1 timeout probe also caught a real regression introduced by the
round-1 watchdog rewrite itself (the fired-event set was unreachable, so no
timeout could ever fire) — fixed and covered by the T2 evidence above.
Manifest re-pinned (
run_policy.first_requests+worker_sources; newdigest
23271da6...in §2 and the guard backstop); guard tampers re-executed(including new
run-policy-first-requestsand note-drift against the newbackstop) with byte-identical restore; full smoke re-run and
verifyDecodeBaselineManifest+ rootcheck+verify.shgreen on the newhead.
Review round 2 (commit 4, head of this branch)
All three findings from the review of head
a2f580dare addressed:a full 0.6b job with
repo_dirset to a throwaway git repo finishednormally and reported that repo's commit as clean, while the code that
actually ran was this checkout).
repo_diris now required to resolveto the repository the worker runs from, anchored on the worker file's
own location — the same check
manifest_pathgets — so the report'scode identity can never name a different checkout. Verified: a
throwaway-
repo_dirjob is rejected (exit 1, failed report, emptyidentity.code).adapters. The reviewer's second option was adopted:
full_generation_sis now the step-region wall time (prefill excluded) in this mode — the
comparable quantity, defined identically on both paths — and the stock
prompt cache is created before the timer starts (symmetric with the
controlled path).
prefill_sis documented in §13.3 as per-adapter andnot cross-adapter comparable: the stock shipped call projects the output
head at every prompt position (about 155 MB of scores at L=512), the
controlled prefill only at the last position.
name is per-process (
report.json.tmp.<pid>), so a second processpointed at the same directory can no longer truncate or delete it
mid-write; a success whose report link is refused is downgraded to
failed(exit 1) — its results are not recorded, so the run must notpresent as success; a temp-write failure removes the temp before
propagating (nothing left behind). Verified at the worker-module level
(W1-W4: refusal leaves a pre-seeded report byte-identical with no temp;
success returns True; second call returns False with the report
unchanged; a write failure propagates with no temp and no report) and
through the full smoke re-run.
Manifest re-pinned twice in this round (final digest
1c691642...in §2and the guard backstop); guard tampers (8/8, byte-identical restore), the
worker negative suites (13 + 14 assertions) and the full smoke (4 workers,
25/25 cross-checks) re-run green on the final head. The T2 timeout probe
(temporary real-manifest tamper, full 26-request mix, 0.6b L=512,
self-correcting deadline placement) re-ran on the final head: in-loop break
at T=7 s (22 complete records, exit 3) on the fixed-continuation cell and
post-loop check at T=15 s (26 complete records,
status: timeout, exit 3);both exits via the main thread (wall < T + 15 s; the 30 s backstop would be
at ≥ T + 29 s); the manifest was restored byte-identical (SHA-256 verified)
and the guard re-passed. The probe also measured the fired lag on this
host: the fired event lands 0.5-1.4 s after the integer-second deadline
(the watchdog thread's first poll is delayed by the main thread's
mlx_lmimport) — the 1 s-poll design bound is unchanged, and the probe'splacement self-corrects on top of it.
verify.shand root./gradlew checkgreen on the final head.
Review round 3 (2026-11-07, head
94fea72)Both findings addressed; contract changes and evidence in findings
§13.4 item 14.
over a running worker's output folder. Each worker now claims the
launcher-created out dir with an exclusive-create
worker-claimfile(
{utc, pid}) before any validation: on an existing claim the workerwrites nothing into the directory (no report, no temp file), reports
failure_reason: output folder already claimed by another workeronstderr and exits 1. The claim is sticky — a killed worker leaves it, so
a claimed directory without a report is dead and the launcher (Java
side, PR Add the Ben Manes versions plugin and upgrade the build dependencies #88) must treat it as such. The report's worker block gains
pid, so a launcher can verify the report came from the process itstarted;
worker-readyis now created with exclusive create too; andthe report-link refusal message is built from the final state and
printed after the success downgrade, so it can no longer say "run
ended as ok" for a run that was downgraded to
failed. Verified by thenew round-3 suite (26 assertions): R1 the reviewer's exact repro (valid
fc worker A running; invalid job B,
first: 0, same folder → B exits 1with no report/temp and the claim left byte-identical; A exits 0 with
26 records and report
pid== claimpid== readypid); R2pre-seeded claim + invalid job → exit 1, nothing written, claim
byte-identical; R3 pre-seeded
worker-ready+ valid job → exit 1 withthe
FileExistsErrorreason, failed report written (the folder wasclaimed by the run itself), ready byte-identical; R4 module-level
(refusal sets the flag and prints nothing; a stale flag is reset at the
call start; the message carries the post-downgrade final status).
full_generation_shad two mode-dependent meanings.Fixed-continuation now records
step_region_s(the step-regionwall time, prefill excluded) with
full_generation_s: null;headline/reference-pair keep
full_generation_s(prefill included)with
step_region_s: null— uniform key set, one value per mode.§13.3 states that decode comparisons use the per-step
steps[].svalues: the step region includes the per-step full-vocabulary
sort-to-host/argmax check work (about 39-46% of the value on the 0.6b
L=512 cell this round: 0.2178 s vs 0.1355 s sum of steps, controlled;
0.2307 s vs 0.1248 s, stock), so differences in the region wall come
from that host work, not from decoding.
Manifest re-pinned (digest
10eaaa0c...in §2 and the guard backstop);guard tampers (8/8, byte-identical restore), the worker negative suites
(13 + 14 — the round-1/2 stderr assertions survive the post-downgrade
message) and the full smoke (4 workers, 30/30 cross-checks including the
new pid/claim and per-mode-key-set checks) re-run green on the final head.
The T2 timeout probe re-ran 11/11 and exposed a probe placement bug
(the worker was correct): the probe aimed the integer-second deadline as
if it sat at
Tin theutc_startorigin, but the worker sets thedeadline right after the
worker-readymarker (before themlx_lmimport and model load), so it sits at
ooff + T(ooff≈ 0.58 s), andthe mid-run fired event lands essentially at the deadline (the watchdog
polls are phase-locked to the anchor; the 0.5-1.4 s first-poll lag of
item 13 applies only during the import). The headline MIX cell's
final-request window (~0.58 s) therefore contained no integer-second
deadline this session, so the round-2 T=15 pass was a host-speed phase
coincidence. The probe now aims in T-space and exercises the post-loop
branch on the reference-pair single-request cell (request starts at
~1.4 s; T=1 sits inside its window across normal host-speed variation —
same branch under test): in-loop break at T=7 s (22 complete records,
exit 3) and post-loop check at T=1 s (1 complete record,
status: timeout, exit 3), both via the main thread (wall < T + 15 s);the manifest was restored byte-identical and the guard re-passed.
verify.shVERIFIED and root./gradlew checkgreen on the final head.Review round 4 (2026-11-07, head
8b1505f)Both findings addressed; contract changes and evidence in findings
§13.4 item 15.
unclear error. The round-3 claim is created before validation, so a
missing
out_dir(or one that is a file) raisedFileNotFoundErrorfrom the claim create in the catch-all handler, and the old
out_dir must be created by the launchercheck invalidatebecamedead code. Fixed:
mainnow checksPath(raw_out_dir).is_dir()before claiming and raises the old message as a
WorkerError(stderr
worker validation failed: out_dir must be created by the launcher, exit 1, nothing written — there is no report target); thedead
validatecheck is removed and the §13.2 claim bullet recordsthe rejection. Verified by new negative tests: T8 (missing dir → exit
1, the launcher message on stderr, no
FileNotFoundError, nothingwritten) and T9 (file in the way → exit 1, same message, the file
byte-identical, no report).
quantity." Both reworded to agree with the updated §13.3 (the step
region is not decode time; decode comparisons use the per-step
svalues) —
controlled.py'srun_fixed_continuationdocstring and thestock fixed-continuation branch in
worker.py. Thecontrolled.pydocstring's value name was also renamed
full_s→step_region_stomatch the report field (the local variable renamed too). Since
rewording
controlled.pymoves its pinned digest, this rides theround-4 re-pin rather than waiting for a later worker change.
Manifest re-pinned (digest
e8c77301...in §2 and the guard backstop);guard tampers (8/8, byte-identical restore), the worker negative suites
(20 + 14 — the 20 now include T8/T9), the round-3 suite (26) and the full
smoke (4 workers, 30/30 cross-checks) re-ran green on the final head. The
T2 timeout probe re-ran 11/11 — the corrected T-space aim landed the
in-loop break on the first attempt (T=8 s, 25 complete records, exit
3) and the post-loop check at T=1 s on the reference-pair cell (1
complete record,
status: timeout, exit 3); both via the main thread(wall < T + 15 s); the manifest was restored byte-identical and the guard
re-passed.
verify.shVERIFIED and root./gradlew checkgreen on thefinal head.