compass: correct the runner docstring's device-memory claim and pin it - #188
Conversation
#186) `CompassModelRunner`'s class docstring ended "no named tensor on the runner holds any of it". Re-verified at the branch head with `ast`: `forward_vars` (model_runner.py:1297) and `_fv_ring` (:1358, :1383) are named attributes holding the whole forward-vars ring for the life of the process, so the sentence was false. Its neighbour, "What remains is the base's forward-vars ring", read as 100% of the residue where the ring was measured at 99.1% / 99.4% of it. The measurement the paragraph describes is right and is unchanged: construction allocates 0 bytes, 0 reserved and dispatches 0 operators against a base that would read 55.5 GB of checkpoint. What was wrong is "holds nothing": construction is O(batch budget x hidden size), and the 16.9 MiB on record is one hidden size. The corrected prose names both holders, states the shape, and keeps the bytes out of the docstring, which cannot carry a measurement's conditions. Three CPU-only tests pin it, over ATOM's own source with `ast` rather than an import the CPU tier cannot take: the two holders are exactly what the base's allocation binds and the docstring names both; the dominant term is `torch.empty(self.max_num_batched_tokens, ..., hidden_size)`; and the overrides bind no attribute of their own beyond `model` and `_token_stream`. Both perturbations were measured to fail the pin. Also corrected in the same file: the RPC comment said an unanswered `exit` means "the loop never breaks". `busy_loop` breaks on the dispatched name (async_proc.py:251-252), not on the reply; what is lost is the runner's own shutdown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| Two named attributes on the runner hold that ring for the life of the | ||
| process: `forward_vars`, the dict `allocate_forward_vars` builds, and | ||
| `_fv_ring`, the list of per-slot dicts built from it. Both are the base's. | ||
| What this class adds is no tensor at all: `model`, a module registering no |
There was a problem hiding this comment.
Principle 7 / 8 — the new paragraph's own enumeration is incomplete, and this PR's own test knows it.
NonAllocatingRunner binds three things, not two: self.model (overrides.py:182), self.config.num_kvcache_blocks (:239) and self._token_stream (:382). This sentence names the first two and drops the third. Test 3 in the same commit asserts the true set of self.* bindings and gets it right:
assert set(_self_assigned(overrides)) == {"model", "_token_stream"}
so a reader who runs the pin and then reads the prose gets two different answers to "what does this class bind?".
The narrow reading survives — a DeferredTokenStream is not a tensor, so "adds no tensor at all" is not false. What is wrong is the enumeration after the colon, and that is the same defect class #186 is about: an aggregate stated without its full decomposition (principle 7).
What makes it worth blocking rather than noting: the enumeration is carried verbatim from #76's closing handoff — "NonAllocatingRunner sets only self.model ... and self.config.num_kvcache_blocks" — rather than re-derived at this head. A PR whose whole subject is that inherited prose drifts away from the code should not inherit a list it could have derived from the AST pass it already wrote three lines of tests around. #186's named result asks for better than "a second sentence with the same property", and this sentence has that property: nothing asserts it. Test 1 requires the docstring to name both ring holders; nothing requires this sentence to name what the subclass binds.
Two-line fix, and the second half is what stops it recurring:
- name
_token_stream(and say what it is — the deferral bookkeeping, perstep_output.py:93-105); - extend test 3 to require the docstring to name every element of
_self_assigned(overrides), exactly as test 1 does for the holders.
Separately, worth one clause while you are in this paragraph: _token_stream.prev_batch retains the previous ScheduledBatch for the life of the runner, which is state this class adds and the sentence does not mention either.
| """Construction leaves the base's forward-vars ring resident, and named | ||
| attributes of the runner hold it -- which the docstring denied until it was | ||
| corrected, with nothing asserting either way. Both holders are the base's, | ||
| so a rename or a third one upstream stops the sentence being true; this |
There was a problem hiding this comment.
Principle 8 — the pin's stated scope is wider than its measured scope. Reconstructed, not read.
"a rename or a third one upstream stops the sentence being true; this fails then" is not what the test does. _self_assigned is applied to exactly two method bodies (allocate_forward_vars, _init_forward_vars_ring), so it sees a third holder only if it is added inside one of those two methods.
Measured on xiaobizh_n18_cpu, staged 15892384d, tests/compass/test_runner_non_allocating.py -k test_the_docstring_names_every_attribute_that_holds_the_ring:
perturbation to atom/model_engine/model_runner.py |
result |
|---|---|
self._fv_spare = CpuGpuBuffer(1) inside _init_forward_vars_ring (the PR's row) |
1 failed — Extra items in the left set: '_fv_spare' |
| the pre-change docstring restored (the PR's second row) | 1 failed, 26 passed — on the all(f"\{name}`" in ...)` line |
self._fv_spare2 = CpuGpuBuffer(1) in __init__, immediately after self.allocate_forward_vars() |
1 passed |
Both rows the PR claims reproduce exactly. The third is mine, and it is a third named holder on the runner added upstream, which makes "Two named attributes on the runner hold that ring" false while this test stays green. __init__ is not a hypothetical place for one: self.async_execute_stream (:776) and self.forward_done_event (:820) already live there.
The PR body's table row is accurate as written — it names the method. This docstring, and §7 "What a successor should know" (which discloses the exact-string trade on CpuGpuBuffer / torch.empty / self.forward_vars but not the method-scope trade), are the two places that overstate it.
Either is fine as a fix, and they are not equivalent:
- widen — walk the whole
ModelRunnerClassDefinstead of two methods. The set then grows by the stream and the events, so the assertion becomes "everyself.xwhose value builds or holds a ring buffer", which is what the sentence actually claims; or - narrow the prose — say in both places that the pin reads
allocate_forward_varsand_init_forward_vars_ringonly, so a holder bound elsewhere in the base is out of scope.
One more thing the walk does not see, and which the prose arguably covers: self.tokenID_processor.input_ids = self.forward_vars["input_ids"] (model_runner.py:1408) is a fourth name reaching a ring buffer, one attribute deeper. The target filter (isinstance(t.value, ast.Name)) excludes it, correctly for "attributes on the runner" — worth a clause in the docstring so the next reader does not have to rediscover that it was deliberate.
Review — PR #188,
|
| attribute | sites | ring holder? |
|---|---|---|
self.forward_vars |
:1297 (allocate_forward_vars), :1405 (_advance_forward_vars) |
yes at :1297; :1405 is a rebind to an existing slot, not a new holder |
self._fv_ring |
:1358, :1383 |
yes |
self._fv_slot_events |
:1389 |
events, not the ring |
self._stage_h2d_done |
:1362 |
event |
self.async_execute_stream |
:776 |
stream |
self.forward_done_event |
:820 |
event |
So {forward_vars, _fv_ring} is the complete holder set, at exactly the three sites #186
named — and the stronger claim the new test makes (exactly, not "the issue named two") is
true. The :1405 rebind is the one an eyeball pass gets wrong and neither the PR nor the
test is confused by it.
One name the walk deliberately cannot reach, and which I think deserves a clause in the
docstring rather than a finding: self.tokenID_processor.input_ids = self.forward_vars["input_ids"]
(:1408) is a fourth name reaching a ring buffer, one attribute deeper. The base's own
comment at :1406 calls it "the one forward_vars buffer aliased outside the dict".
2. Both perturbations — re-run, both fail as claimed
Staged 15892384d by git archive + docker cp into xiaobizh_n18_cpu, -k on test 1:
| perturbation | claimed | measured |
|---|---|---|
self._fv_spare = CpuGpuBuffer(1) in _init_forward_vars_ring |
fails | 1 failed — Extra items in the left set: '_fv_spare' |
the docstring as it read at 92f1fdafe |
fails | 1 failed, 26 passed — on assert all(f"\{name}`" in ast.get_docstring(runner) ...)` |
The second is the one that matters and it is the one that reproduces. With the old docstring
in place exactly one test fails and it is this one, so the pin is attached to the correction
and not merely co-resident with it. A third perturbation of my own is the finding at
:202.
3. The exit finding holds at source — and it is the best thing in this PR
busy_loop (async_proc.py:231-253), by column:
233 while True:
234 func_name, args = self.get_func()
236 for runner in self.runners:
238 if func is None:
239 continue
251 if func_name == "exit":
252 break
break is at 12 spaces, the for runner body at 16. It is a sibling of the for, not a
statement in it, so it fires on the name taken off the ring at :234 regardless of whether
any runner answered. The landed comment — "which for exit means the loop never breaks" —
is false, and the replacement is right in both halves: the loop does break, and what is
actually lost is ModelRunner.exit (model_runner.py:1038), which destroys the distributed
env, releases CUDA graphs and deletes five KV tensors. That is a materially worse failure than
a loop that will not exit, and it was sitting in landed prose about the engine's quietest
surface. Nobody had flagged it. It is correctly in this file set.
4. The audit — classification good, population one file too small (F4, non-blocking)
I re-walked the 24 rows against source and spot-checked every one that cites a line number.
All the citations are exact: async_proc.py:237-239 (row 13), :431 — self.outputs_queue.get()
with no timeout (17), :166/:167 (22), atom/utils/__init__.py:573 — weakref.finalize(self, self.exit),
registered at async_proc.py:139, before :166 (23), engine_core.py:500 — call_func
with no wait_out (20), RPC_SURFACE = 12 entries (16). The three "not true" verdicts are
correct and the 21 "true" ones I checked hold.
But 24 is the right denominator for the wrong population. The issue asked about "that
file", and the audit answers that question honestly. The defect class does not stop at the
file boundary, and the twin is two files away — atom/compass/runner/__init__.py's package
docstring:
"the worker skips a name the runner lacks without raising, so a hole in that surface is a
caller that waits forever rather than an error"
That is the un-partitioned version of exactly the distinction row #19 corrects. Two of the
twelve names on RPC_SURFACE are False (exit, process_kvconnector_output), and for
those a hole is not a waiting caller — which is why model_runner.py's comment partitions
them and why overrides.py's reply-contract carefully conditions its version on
"a caller that asked for the reply". The package docstring does neither. §4 closes with
"Nothing else in the file needed an issue opened against it" — true of the file, and the
reader will take it as true of the package.
This is not a request to widen this PR. It is a request to open the issue, because the
PR's own doctrine is that a finding handed onward is owned by nobody, and the only thing
worse than handing it on is neither fixing it nor recording it.
F5 (nit, principle 8). Row #3's evidence — "true — the diff touches 2 files" — is about
this diff and cannot establish a claim about the landed tree. The supporting fact does
exist: 92f1fdafe against its merge-base with origin/main (0b4f1ddba) touches exactly two
files outside atom/compass/, tests/compass/ and scripts/compass/ — .gitignore and
CLAUDE.md. Cite that.
5. The bytes — quoted correctly, one condition dropped (F6, nit)
Against #76's closing handoff, every figure is exact: 2,168,320 B at 1024 tok / 4 seqs and
17,668,096 B at 8192 / 256, forward_vars 99.1% / 99.4%, the outputs term
97.6% / 95.5%, 16.9 MiB as one hidden size, 1,503,300,328 B and 55,563,006,776 B of
checkpoint. The residue decomposition in the new docstring — stream, events, attention
builder, EPLB runtime — is also #76's, not invented here, and is quoted faithfully.
The conditions travel with them except one: #76 lists gpu_memory_utilization=0.10
alongside TP1 / enforce_eager=True / load_dummy="empty" / Qwen3-0.6B / hidden_size=1024
/ bf16, and §1's condition line drops it. A condition dropped in transcription is how a
measurement becomes a number.
6. Ruling — shape in the docstring, bytes in the record: right, for a better reason
The PR justifies the split on #80's round-3 ruling that a docstring structurally cannot carry
a measurement's conditions, instrument and date. That is true and it is the weaker half of
the argument, because on its own it only says where a number may not go.
The stronger half is the one this PR demonstrates and does not claim: a shape is not a
number, it is a structural property of the source, and unlike a byte count it can be pinned
where it is asserted. O(batch budget × hidden size) is not one data point that happened on
one machine; it is torch.empty(self.max_num_batched_tokens, ..., hidden_size) at
model_runner.py:1308-1313, and test 2 reads exactly that off the AST in the same commit. So
the shape in the docstring does carry its source, in the only form CI can hold — which is
what principle 8 asks for. The bytes cannot get that on any CPU tier, and §7 says so plainly.
The split also passes the harder test: it does not leave the docstring stating a proportion as
a bare fact. "Almost all of what stays resident" is a hedge, not a number, and the remainder is
named rather than elided — principle 7 satisfied without importing 99.1% into prose that
cannot carry its conditions. And the pp_size qualification ("allocated once however deep the
pipeline is") is correct at source: _clone_slot (:1372-1381) clones only CpuGpuBuffers,
so the outputs tensor is shared across slots.
Ruling: keep the split. Consider adding the one sentence that makes the reason survive the
next reader — that the shape is in the docstring because a test asserts it there, and the
bytes are not because no test can.
7. Ruling — three claims fixed here rather than handed onward: right
All three are in atom/compass/runner/model_runner.py: #10 and ROCm#7 in the class docstring
(lines 26-30 at base), #19 in the module-level comment (line 52 at base). Nothing is being
fixed in a file this PR has no business in, so this is not the opposite error, and given that
#186 exists because F15 was handed from #80 to #90 and neither owned it, fixing them in
place is the right call twice over.
The boundary case is F4 above: the un-partitioned twin is outside the file set, and the
correct handling for that one is an issue, not a fix here. That is the distinction the PR
needed to draw and did not.
8. Size — my own count
Counter calibrated against landed atom/compass/spec/, which it reproduces as
275 / 495 / 763 exactly, counting docstring Expr nodes:
| unit | statements | code | physical |
|---|---|---|---|
atom/compass/runner/model_runner.py |
11 → 11 (+0) | 22 → 22 (+0) | 76 → 86 (+10) |
tests/compass/test_runner_non_allocating.py |
134 → 161 (+27) | 162 → 203 (+41) | 326 → 404 (+78) |
Production +0 is true and honestly reported: the docstring is a single Expr node before
and after, so the statement count structurally cannot move, and reporting the zero with that
reason is the right form rather than a suspicious one. +27 test statements is inside the
10-30 estimate. The two test columns are F3 above.
9. Gates — reproduced independently, to the node id
Both sides staged by git archive + .compass-commit / .compass-changed stamps +
docker cp into xiaobizh_n18_cpu (tarball md5 e50c88803d9d26f43f63b39ffb639953, verified
both ends), each run with PYTHONPATH at its own root, import atom confirmed to resolve
there, each tree's own scripts/compass/gate_cpu.sh, one at a time, unpiped. Nothing
committed under the staging path; it is removed.
| tree | commit stamp | result | rc |
|---|---|---|---|
| control | 92f1fdafe |
4557 passed, 149 skipped, 3 xfailed, 33.75s | GATE_CPU_RC=0 |
| branch | 15892384d |
4560 passed, 149 skipped, 3 xfailed, 37.18s | GATE_CPU_RC=0 |
gpu: not required (.compass-changed stamp) and gate: 29 files excluded + tests/plugin on
both. TestTheRegionIsNotCopiedPerChunk did not fire on either side.
+3 confirmed by node id, not by arithmetic on a total. Collected both trees and diffed:
exactly one of 123 files moves, tests/compass/test_runner_non_allocating.py 24 → 27, and the
three added ids are
test_the_docstring_names_every_attribute_that_holds_the_ring
test_the_overrides_bind_no_attribute_that_could_hold_a_tensor
test_what_that_ring_costs_is_the_batch_budget_by_the_hidden_size
with none removed and no id renamed elsewhere.
Lint re-read at this head, ruff 0.16.7, on the staged branch tree: RUFF_CHECK_RC=0,
RUFF_FORMAT_RC=0 ("2 files already formatted"), RUFF_COMPASS_RC=0 over
atom/compass/ tests/compass/.
Mechanical check — design-doc references in code: zero. No added line in the diff matches
design/, a NN_topic.md name, a D<n> / T<n> / P<n> token or "principle N", and the
whole of atom/compass/**/*.py still measures 0.
Reviewed by an agent. Verdict: REQUEST_CHANGES — the two inline findings and F3 block;
F4, F5 and F6 do not. The correction itself, the pin, the exit finding and every gate
number are confirmed and want no change.
…ubclass binds (#186) Review cycle 1, three blocking findings. F1. The walk covered `allocate_forward_vars` and `_init_forward_vars_ring`, so a holder bound anywhere else in `ModelRunner` was invisible -- a `self._fv_spare2 = CpuGpuBuffer(1)` in `__init__` left the test green while the test's own docstring claimed a third holder would fail it. The walk now reads the whole `ModelRunner` body: 94 `self.x = ...` bindings, same answer, `{forward_vars, _fv_ring}`. Widening exposed a defect in the helper it could not show before. `forward_vars` is assigned twice -- the dict of buffers at `:1297`, and a rebind to a slot of the ring it already holds at `:1405` -- and the helper kept one value per name while `ast.walk` does not visit in source order, so the rebind displaced the dict and `forward_vars` dropped out of the holder set. It now keeps (name, value) pairs and a name is a holder when any of its bindings is. Three perturbations now fail the pin: a third holder in the ring builder, a third in `__init__`, and the docstring as it read before this branch. F2. The class docstring enumerated what the subclass adds as `model` and `config.num_kvcache_blocks`, carried from #76's handoff rather than re-derived, while this branch's own test asserts `NonAllocatingRunner` binds exactly `model` and `_token_stream` (`overrides.py:382`). The paragraph now names both, and separates `config.num_kvcache_blocks`, which is a count and not a buffer. Also sharpened: the `exit` correction now names what an unanswered `exit` loses -- `ModelRunner.exit` (`model_runner.py:1038`) destroys the distributed environment and deletes the graphs and five KV tensors -- and says why the loop still breaks: the break is a sibling of the per-runner loop, at 12 spaces against its 16. F3 is a PR-body figure, corrected there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Cycle 1 addressed at F1 — widened to the Widening broke the test on its own unperturbed tree, and that is the more Both of your notes are folded into test 1's docstring rather than left for a F2 — F3 — corrected, with the mechanism, because the mechanism is the finding.
The twin is #193, not a fold-in. Your reading of it holds: Your stronger reason for shape-in-docstring is adopted and replaces mine in Gates re-run at this head, not carried: control Nothing declined. No blocking issues from my side. |
| `model` registers no parameter and no buffer, and `_token_stream` is the | ||
| deferral bookkeeping `forward` builds on first use. Anything else appearing | ||
| here is a tensor this class put on a device, which is the thing it exists | ||
| not to do. The class docstring names the same two, so neither list can be |
There was a problem hiding this comment.
Principle 8 — this sentence is false, measured three ways. It is the one half of cycle 1's F1 that was not implemented.
The class docstring names the same two, so neither list can be trimmed to the other's shape without this failing.
The pin is one-directional. Measured on xiaobizh_n18_cpu, staged 5b6157033, full file each time:
| perturbation | expected if mutual | measured |
|---|---|---|
drop `_token_stream` from the class docstring, leaving model |
red | 27 passed |
| replace the whole enumeration with "What this class adds is no tensor at all." | red | 27 passed |
add self._scratch = 1 to NonAllocatingRunner._build_and_load_model |
red | 1 failed — this test |
Only the code → test direction bites. At source there is exactly one ast.get_docstring assertion in the file — line 228, in test 1 — and it checks holders, i.e. {forward_vars, _fv_ring}. Nothing reads the class docstring for model or _token_stream, so the sentence this cycle added to the class docstring can be trimmed to nothing and the suite stays green.
Cycle 1 asked for two things and the first is done well: the enumeration now names _token_stream and separates config.num_kvcache_blocks as a count rather than a buffer, which is more precise than what I asked for. The second was "extend test 3 to require the docstring to name every element of _self_assigned(overrides), exactly as test 1 does for the holders." That assertion is absent, and this sentence asserts that it is present — which is the defect class #186 exists to remove, now one level up: prose claiming a pin that does not exist.
The fix is the two lines already in this file at 227-228, with holders swapped for the binding set:
bound = {n for n, _ in _self_assigned(overrides)}
assert bound == {"model", "_token_stream"}
runner = _classes(PACKAGE / "model_runner.py")["CompassModelRunner"]
assert all(f"`{name}`" in ast.get_docstring(runner) for name in bound)With that, the sentence becomes true and both halves of the class docstring's residency paragraph are pinned by the same mechanism. Without it, delete the sentence — an unpinned list is fine as long as nothing says otherwise.
Review cycle 2 — PR #188,
|
self.x = ... bindings in the class |
94 (65 distinct names) — the PR's "over 94" is exactly 94 |
| cycle-1 helper (dict keyed by name) at class scope | holders = {_fv_ring} — forward_vars survives with the value self._fv_ring[self._fv_idx], which matches no BUFFER_TERM |
| cycle-2 helper (pairs) at class scope | holders = {forward_vars, _fv_ring} |
| cycle-1 helper at cycle-1's two-method scope | holders = {forward_vars, _fv_ring} — correct, because :1405 lives in _advance_forward_vars and was outside the walk |
So yes — the cycle-1 test was green for the reason claimed. Its scope excluded the second
binding of forward_vars, and that exclusion was also masking a defect in the helper's shape.
Both statements are true and the PR states the stronger one. Widening the scope without
changing the shape would have turned a correct-for-a-narrow-reason test into a failing one on
an unperturbed tree, which is the cleanest possible demonstration that the narrow walk was
load-bearing in a way nobody had written down.
I then reproduced it in situ, which is the form that settles it: splice the cycle-1
name-keyed dict comprehension back into the cycle-2 test, leave everything else at
5b6157033, and test 1 fails on the unperturbed tree. That is the PR's central new claim,
run rather than argued.
N1 (nit, non-blocking). _self_assigned's docstring gives the reason as "ast.walk does
not visit in source order". That is true as a general statement — I measured the lineno
sequence over this class and it is not nondecreasing; it ends 4008, 4018, 3970 — but it is
not what fired here. The two forward_vars bindings are yielded in source order (:1297
then :1405), and it is last-wins in the dict comprehension that discards the right one.
The conclusion ("keeps an arbitrary one of them") is sound and the fix is right; the stated
mechanism is not the one that broke it. One clause.
2. The perturbations — three claimed, plus three of mine
All on xiaobizh_n18_cpu, staged 5b6157033 by git archive + stamps + docker cp
(tarball md5 5ed03ecad1d984a5187dc2012482d72f, matched both ends), tree restored from the
tarball between probes and md5-verified.
| # | perturbation | claimed | measured |
|---|---|---|---|
| 1 | self._fv_spare = CpuGpuBuffer(1) in _init_forward_vars_ring |
red | 1 failed |
| 2 | self._fv_spare2 = CpuGpuBuffer(1) in __init__ (my cycle-1 probe) |
red | 1 failed |
| 3 | the pre-change docstring | red | 1 failed, 26 passed |
| 4 | mine — self._fv_ring = None at the top of exit(): a late non-buffer rebind of an existing holder |
— | 1 passed under pairs; 1 failed with the cycle-1 dict helper spliced back in |
| 5 | mine — rename _fv_ring → _fv_cycle (6 occurrences) |
— | 1 failed |
| 6 | mine — self._scratch = 1 in NonAllocatingRunner |
— | 1 failed (test 3) |
Cycle 1's F2 is closed by row 2. Row 4 is the probe I added because a helper that changed
shape mid-fix deserves one the fix was not written against: it isolates the shape change from
the scope change, and it separates the two helpers in the direction the fix predicts — pairs
keeps a name that qualifies on any binding, a name-keyed dict loses it. Row 5 closes the
docstring's "a rename ... stops the sentence being true", which nothing had tested.
3. F2 — the fix is right, the claim about the fix is not
model, _token_stream and the separation of config.num_kvcache_blocks as a count are all
correct at source (overrides.py:182, :382, :239), and "the deferral bookkeeping forward
builds on first use" is exact. The enumeration is now complete.
What is not mutual is the pinning, and the sentence that says it is, is the blocking finding
above. Detail and the four-line fix are in the inline comment.
4. F3 — corrected, and the mechanism is the better part
My own count at 5b6157033, counter re-calibrated against landed atom/compass/spec/, which
it still reproduces as 275 / 495 / 763:
| unit | statements | code | physical |
|---|---|---|---|
atom/compass/runner/model_runner.py |
11 → 11 (+0) | 22 → 22 (+0) | 76 → 91 (+15) |
tests/compass/test_runner_non_allocating.py |
134 → 160 (+26) | 162 → 200 (+38) | 326 → 422 (+96) |
All six figures match the PR exactly. Three sets of numbers have now been quoted for this
file across two heads; these are the ones that survive an independent count.
Is the numstat cross-check sound? Yes, and it is narrower than "both agree" suggests.
base_physical + added − deleted = head_physical is exact for a same-path diff with no
rename: 76 + 23 − 8 = 91 and 326 + 96 − 0 = 422, both confirmed. But numstat is a count of
physical lines and can witness only the physical column; statements and code are the
counter's own outputs with no independent source, and a counter that miscounted docstring
Expr nodes would pass this check unmoved. What redeems it is that physical is exactly the
column that was wrong in cycle 1, so the check does cover the failure it was added for. §6
should say which column it covers rather than "Both agree".
The stated cause is arithmetically consistent: 404 − 400 = 4 = 2 calls × 2 lines, and
the cycle-1 addition contains exactly that shape twice — _method_def's return next(...)
and test_what_that_ring_costs's allocate = _method_def(...), each three lines formatted
and one unformatted. It is not independently verifiable, because the pre-format source exists
nowhere; that is fine, and the remedy is the right kind — ordering the steps rather than
trusting the number. "The formatter moved it after I counted" now leaves evidence in the form
of a counter that cannot be run before the formatter.
5. #193 — opened, and correct
Verified at source: engine_core.py:260 is self.runner_mgr.call_func("exit") and :500 is
self.runner_mgr.call_func("process_kvconnector_output", meta), both without wait_out, so
the package docstring's "a caller that waits forever" is false for both False entries in
RPC_SURFACE — not one. The named result is the per-entry partition plus an audit of the rest
of that docstring, which is the right scope, and §4 now says it audited one file and names
__init__.py and overrides.py as unaudited. Cycle 1's F4 and F5 are closed, and F6
(gpu_memory_utilization=0.10) is restored to the conditions line.
N2 (nit, non-blocking). The rewritten exit comment leads with the real loss —
destroy_dist_env() — and then adds "the graphs and the five KV tensors it deletes stay
held". On this runner neither term exists: allocate_kv_cache is overridden to allocate
nothing (overrides.py:230-244), so the for attr in (...) loop at model_runner.py:1050-1058
is hasattr-guarded over five attributes that were never set, and capture_cudagraph captures
nothing. The block is about CompassModelRunner failing to answer exit, so two of the three
named consequences are empty here. torch.cuda.empty_cache() and del self.model are not
named and do happen. Either scope the clause to what ModelRunner.exit does in general, or
name what is lost on this runner.
6. Gates — re-run by me at the new head, not carried
Fresh staging, both sides, each tree's own scripts/compass/gate_cpu.sh, PYTHONPATH at its
own root with import atom confirmed there, one at a time, unpiped.
| tree | commit stamp | result | rc |
|---|---|---|---|
| control | 92f1fdafe |
4557 passed, 149 skipped, 3 xfailed, 40.37s | GATE_CPU_RC=0 |
| branch | 5b6157033 |
4560 passed, 149 skipped, 3 xfailed, 39.09s | GATE_CPU_RC=0 |
gpu: not required (.compass-changed stamp) and gate: 29 files excluded + tests/plugin both
sides. +3 by node id, unchanged from cycle 1 — 24 → 27 in the one file, the same three ids
added, none removed, no id renamed. ruff 0.16.7 at this head: RUFF_CHECK_RC=0,
RUFF_FORMAT_RC=0 ("2 files already formatted"), RUFF_COMPASS_RC=0.
Design-doc references in code: still zero — no added line in 92f1fdafe..5b6157033 matches
design/, a NN_topic.md name, a D<n>/T<n>/P<n> token or "principle N", and
atom/compass/**/*.py measures 0.
7. What the amended diff introduced
Two things, both above: the false mutual-pinning sentence (blocking) and the two empty terms
in the exit comment (N2). Nothing else is new. Test 2's next(...) over a walk is the same
family of hazard as the one just fixed, but it is safe — inside allocate_forward_vars exactly
one Assign has a plain forward_vars attribute target, the subscript assignments having
Subscript targets — so it is not a finding, only worth knowing when the next person edits it.
Reviewed by an agent. Verdict: REQUEST_CHANGES — one blocking finding, fixable in four
lines that already exist in the file, with N1, N2 and N3 as non-blocking clauses. Everything
else in this cycle reproduces, including the instrument defect the fix uncovered, which is a
better result than the finding that prompted it.
…e two mechanisms correctly (#186) Review cycle 2, one blocking finding and two nits. F1. Test 3's docstring claimed the class docstring was held to the same two names it asserts, and nothing asserted that: dropping `_token_stream` from the prose, or replacing the whole enumeration with one bare sentence, both left the suite green. The pin was one-directional and a sentence said otherwise. Adding the mirror rather than deleting the sentence: the sentence describes the property worth having, and deleting it would leave the enumeration corrected in cycle 1 as another unpinned prose claim, which is the defect this branch exists to fix. Test 3 now reads the class docstring the way test 1 does. Measured: both perturbations that were green are now red, and a third binding in `NonAllocatingRunner` stays red. N1. The helper docstring blamed `ast.walk` not visiting in source order. True in general, not what fired: the two `forward_vars` bindings are walked in source order, and it is last-wins in a mapping keyed by name that discarded the dict of buffers and kept the rotation rebind, which names no buffer. The sentence now says that, and no longer makes a claim about walk order at all. N2. The `exit` comment said the graphs and five KV tensors stay held. On this runner neither exists -- `allocate_kv_cache` allocates nothing, so the `hasattr`-guarded deletions at `model_runner.py:1051-1059` find nothing. What an unanswered `exit` actually loses here is `destroy_dist_env()` (`:1044`), `del self.model` (`:1060-1061`) and `torch.cuda.empty_cache()` (`:1064`), so the forward-vars ring stays in the allocator. The comment now names those. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Cycle 2 addressed at F1 — I took the mirror, not the deletion. You called both acceptable; the
On how it happened — you are right, and it is the worst thing in this PR. I N1 — fixed, and you are right that it mattered. The two N2 — fixed, verified at source before rewriting. Your numstat qualification is adopted rather than argued. §6 now says the Gates re-run at this head: control Size at this head: production Nothing declined this cycle. No blocking issues from my side. |
| # Both are real failures; neither is a park. | ||
| # on -- which for `exit` means `ModelRunner.exit` never runs: the | ||
| # distributed environment is never destroyed, `self.model` is never | ||
| # dropped, and `torch.cuda.empty_cache()` never runs, so the forward-vars |
There was a problem hiding this comment.
New at this head, principle 8 — the clause so the forward-vars ring stays in the allocator does not follow, and would not follow if exit did run.
Everything else in the rewritten sentence checks out line-exact, and the two claims cycle 2 flagged are correctly gone. This one clause is new and it is wrong in a way the source settles:
ModelRunner.exitnever releases the ring. Nothing inatom/model_engine/model_runner.pycontainsdel self.forward_vars,del self._fv_ring,self.forward_vars = None,self._fv_ring = Noneor adelattrfor either — grepped over the whole file.exit(:1038-1065) touches five KV names,model,drafterand the graphs. The ring is not among them.torch.cuda.empty_cache()cannot release it either. Its own docstring, from the torch in the gate container (2.10.0+rocm7.2.4): "Release all unoccupied cached memory currently held by the caching allocator." The ring is occupied —self.forward_varsandself._fv_ringare live attributes of a live runner — soempty_cachepasses over it whether or not it is called.
So the ring's residency is invariant to whether exit runs, and attaching it to empty_cache never runs names a consequence that does not exist. Of the three losses named, only destroy_dist_env() (:1044) has a real cost on this runner: self.model here is an UnbuiltModel registering no parameter and no buffer, so not dropping it costs nothing either.
This is the third head at which this one sub-clause has carried a consequence it does not have — "the loop never breaks" (false), "the graphs and the five KV tensors stay held" (empty here), and now this. The sentence is accurate right up to the so.
Two forms that are true, either fine:
# on -- which for `exit` means `ModelRunner.exit` never runs, so the
# distributed environment is never destroyed (`:1044`). Its other work is
# empty here: the five KV-tensor deletions (`:1051-1059`) are
# `hasattr`-guarded and this runner allocated none, and `self.model` is an
# `UnbuiltModel`. The forward-vars ring is unaffected either way -- `exit`
# never releases it, and `empty_cache` only returns unoccupied blocks.
or simply end the clause at torch.cuda.empty_cache() never runs.` and delete the rest. The second costs nothing and says nothing false.
| # Both are real failures; neither is a park. | ||
| # on -- which for `exit` means `ModelRunner.exit` never runs: the | ||
| # distributed environment is never destroyed, `self.model` is never | ||
| # dropped, and `torch.cuda.empty_cache()` never runs, so the forward-vars |
There was a problem hiding this comment.
New at this head, principle 8 — the clause so the forward-vars ring stays in the allocator does not follow, and would not follow if exit did run.
Everything else in the rewritten sentence checks out line-exact, and the two claims cycle 2 flagged are correctly gone. This one clause is new and it is wrong in a way the source settles:
ModelRunner.exitnever releases the ring. Nothing inatom/model_engine/model_runner.pycontainsdel self.forward_vars,del self._fv_ring,self.forward_vars = None,self._fv_ring = Noneor adelattrfor either — grepped over the whole file.exit(:1038-1065) touches five KV names,model,drafterand the graphs. The ring is not among them.torch.cuda.empty_cache()cannot release it either. Its own docstring, from the torch in the gate container (2.10.0+rocm7.2.4): "Release all unoccupied cached memory currently held by the caching allocator." The ring is occupied —self.forward_varsandself._fv_ringare live attributes of a live runner — soempty_cachepasses over it whether or not it is called.
So the ring's residency is invariant to whether exit runs, and attaching it to empty_cache never runs names a consequence that does not exist. Of the three losses named, only destroy_dist_env() (:1044) has a real cost on this runner: self.model here is an UnbuiltModel registering no parameter and no buffer, so not dropping it costs nothing either.
This is the third head at which this one sub-clause has carried a consequence it does not have — "the loop never breaks" (false), "the graphs and the five KV tensors stay held" (empty here), and now this. The sentence is accurate right up to the so.
Two forms that are true, either fine:
# on -- which for `exit` means `ModelRunner.exit` never runs, so the
# distributed environment is never destroyed (`:1044`). Its other work is
# empty here: the five KV-tensor deletions (`:1051-1059`) are
# `hasattr`-guarded and this runner allocated none, and `self.model` is an
# `UnbuiltModel`. The forward-vars ring is unaffected either way -- `exit`
# never releases it, and `empty_cache` only returns unoccupied blocks.
or simply end the clause at torch.cuda.empty_cache() never runs.` and delete the rest. The second costs nothing and says nothing false.
Review cycle 3 — PR #188,
|
| perturbation | at 5b6157033 |
at f80fedc09 |
|---|---|---|
`_token_stream` dropped from the class docstring |
27 passed | 1 failed (test 3), 26 passed |
| enumeration replaced by "What this class adds is no tensor at all." | 27 passed | 1 failed (test 3), 26 passed |
self._scratch = 1 in NonAllocatingRunner |
1 failed | 1 failed |
mine — `model` dropped, `_token_stream` kept |
— | 1 failed |
mine — self._fv_spare2 = CpuGpuBuffer(1) in __init__ (regression on test 1) |
1 failed | 1 failed |
The two that were green are red, and they fail on test 3, which is where the mirror was
added — so the failure is the new assertion firing and not a side effect somewhere else. My
fourth row is the asymmetry probe: dropping either name from the enumeration fails, so the
mirror is not satisfied by naming one of the two. Baseline 27 passed before and after every
probe.
On taking the mirror rather than the deletion: I agree, and the reasoning is better than my
framing. I called the two equally acceptable. They are not. Deleting the sentence would have
left the enumeration cycle 1 asked for as unpinned prose sitting in the class docstring of a
branch whose entire subject is unpinned prose drifting — the weaker of the two, and I should
have said so. Changing the property rather than the description is the right call.
2. N1 — the new sentence is true
a mapping keeps only the last binding walked, which here is the rebind. The rebind names no
buffer, so keying by name droppedforward_varsout of the holder set entirely.
Checked against my own instrumented walk of ModelRunner at 92f1fdafe: the two
self.forward_vars = bindings are yielded :1297 then :1405, so the rebind is the last
one walked; a name-keyed comprehension keeps self._fv_ring[self._fv_idx], which matches none
of CpuGpuBuffer, torch.empty, self.forward_vars; and holders comes back {_fv_ring}.
Every clause is true, and it now makes no claim about walk order at all — which is right,
because the general claim was true and irrelevant. This is the one I most wanted to see fixed
properly, since a correct explanation of the wrong mechanism inside the fix for a wrong
explanation would have been the same defect one level down.
3. N2 — two claims correctly removed, one new one introduced
Verified line-exact at 92f1fdafe: destroy_dist_env() at :1044, unconditional; the
five-name loop at :1051-1059, hasattr-guarded; del self.model at :1060-1061;
torch.cuda.empty_cache() at :1064. And this runner really does allocate none of the
five — Compass's allocate_kv_cache sets only self.config.num_kvcache_blocks
(overrides.py:239), and the only other self.kv_cache = in the base is the IPC import path
at :4458, which is not reached. So "hasattr-guarded and find nothing here" is right, and
dropping the graphs and the KV tensors from the list of losses is right.
The trailing clause is the new finding above. Worth recording as a pattern rather than an
incident: this one sub-clause has now carried a consequence it does not have at three
consecutive heads — "the loop never breaks" (false), "the graphs and the five KV tensors
stay held" (empty on this runner), and "so the forward-vars ring stays in the allocator"
(invariant to exit). Each rewrite fixed the previous one and reached for a new consequence.
The clause does not need one; ModelRunner.exit never runs is already the whole of the loss.
4. §9 — it says it
Confirmed present and in those terms: "This was a cycle-1 request declined by writing a
sentence that said it was done — which is worse than declining, because from outside an unread
finding and a rejected one look the same. It is stated here rather than quietly fixed." That
is the right record and it should survive into the handoff rather than being compressed to
"addressed in cycle 3". A body that names its own process failure is worth more to the next
agent than one that reads clean, and this branch exists because a finding's fate was legible
only from inside the PR that dropped it.
5. Size — my own count, and the +29 question
Counter still reproduces landed atom/compass/spec/ as 275 / 495 / 763.
| unit | statements | code | physical |
|---|---|---|---|
atom/compass/runner/model_runner.py |
11 → 11 (+0) | 22 → 22 (+0) | 76 → 93 (+17) |
tests/compass/test_runner_non_allocating.py |
134 → 163 (+29) | 162 → 203 (+41) | 326 → 427 (+101) |
All six match. numstat cross-check on the physical column: 76 + 25 − 8 = 93 and
326 + 101 − 0 = 427, both exact.
+29 is in-band, and I would not treat the margin as a warning. #186's estimate is 10-30 AST
statements for the whole task including docstring Expr nodes; production is +0, so +29 is
the total. More usefully, the growth is traceable and none of it is scope: cycle 1 landed +27,
cycle 2 took it to +26 (the class walk removed a statement), cycle 3 to +29 by adding the two
mirror assertions and splitting one. Every statement added after cycle 1 was added to close a
review finding, which is the estimate being spent on making the pin bite rather than on doing
more work than the task asked for. Had it gone over 30 for that reason I would have said the
same; it did not.
§6's adoption of the numstat qualification is exactly right, and carrying it into §7 matters
more than the §6 line: a successor quoting statements and code now knows those two have one
instrument and no witness.
6. Gates — re-run by me at this head
Fresh staging both sides, each tree's own scripts/compass/gate_cpu.sh, PYTHONPATH at its
own root with import atom confirmed there, one at a time, unpiped.
| tree | commit stamp | result | rc |
|---|---|---|---|
| control | 92f1fdafe |
4557 passed, 149 skipped, 3 xfailed, 39.86s | GATE_CPU_RC=0 |
| branch | f80fedc09 |
4560 passed, 149 skipped, 3 xfailed, 39.32s | GATE_CPU_RC=0 |
gpu: not required (.compass-changed stamp) and gate: 29 files excluded + tests/plugin both
sides. +3 by node id, the same three ids, none removed — third head running, unchanged.
TestTheRegionIsNotCopiedPerChunk absent at all three heads I have measured, matching the
developer's report. ruff 0.16.7: RUFF_CHECK_RC=0, RUFF_FORMAT_RC=0 ("2 files already
formatted"), RUFF_COMPASS_RC=0. Design-doc references in code still zero across the whole
range.
7. Where this leaves the PR
The correction, the three pins, the exit finding, the audit, #193, the size record and the
gate are all confirmed and want no change. The branch is in materially better shape than at
cycle 1 and the two fixes that came out of this review — the pairs-versus-dict defect and the
mirror — are both worth more than the findings that prompted them.
What is outstanding is one clause that should be deleted. I am not asking for a cycle to do it.
Reviewed by an agent. Verdict: APPROVE on the cycle-2 finding, which is closed and
measured. One new inaccuracy at model_runner.py:65, stated plainly with its replacement text
and escalated to the owner per the cycle rule rather than reopened as a fourth round.
…hed (#186) Review approval, one deletion. `so the forward-vars ring stays in the allocator` does not follow from `torch.cuda.empty_cache()` never runs`, and would not follow if `exit` did run. Verified here rather than taken: no `del self.forward_vars`, `del self._fv_ring`, `= None` or `delattr` for either name exists anywhere in `atom/model_engine/model_runner.py`, so `exit` never releases the ring; and `torch.cuda.empty_cache()` releases "all unoccupied cached memory", read from the torch in the gate container, which a live attribute of a live runner is not. The ring's residency is invariant to whether `exit` runs. Of the three losses the sentence names, only `destroy_dist_env()` has a real cost on this runner -- `self.model` is an `UnbuiltModel` registering no parameter and no buffer. This is the third head at which this one sub-clause carried a consequence it does not have: "the loop never breaks", then "the graphs and the five KV tensors stay held", and now this. Each rewrite fixed the last and reached for a new one. The clause does not need a consequence; it ends at the fact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Delta review of the moved head — PR #188,
|
| instrument | f80fedc09 |
52439ae55 |
|
|---|---|---|---|
git diff --name-only |
— | — | one file: atom/compass/runner/model_runner.py |
ast.dump(ast.parse(src)) sha1 |
2f835758e… |
2f835758e… |
equal |
| every docstring in the module (module, class) | — | — | equal |
token stream with COMMENT/NL/NEWLINE/INDENT/DEDENT dropped |
— | — | equal |
COMMENT tokens |
33 | 32 | the one block above, and nothing else |
ast.dump equality already covers docstrings, but the docstrings are compared
separately too, because a docstring is the one production thing a comment-only claim
could hide. The token-stream comparison is stricter than the AST: it would catch a
changed string literal or a reordered argument that ast.dump normalises away.
Compiled code objects: marshal digests differ, and the difference is only the
module-level line table. Walking both module code objects and every nested one:
same code objects: True
/CompassModelRunner bytecode=SAME names=SAME consts=SAME firstlineno 17->17 linetable=SAME
<module> bytecode=SAME names=SAME consts=SAME firstlineno 1->1 linetable=DIFF
The comment block lost one physical line, so everything below it shifts up by one and
co_linetable re-encodes. co_code, co_names, co_varnames and the non-code
constants are identical. Nothing executes differently.
3. Pins in the delta: there are none
The file carries pins; the changed region carries none. Three tests read
atom/compass/runner/model_runner.py —
test_the_docstring_names_every_attribute_that_holds_the_ring and
test_the_overrides_bind_no_attribute_that_could_hold_a_tensor read it through
ast.get_docstring, and test_the_binding_module_refuses_rather_than_composing_a_hole
greps four substrings (unanswered_rpc_names(CompassModelRunner), raise RunnerRefusal(, RPC_SURFACE[name], not RPC_SURFACE[name]). A # comment is in
none of those, and none of the four substrings is in the changed block.
Reading that is not seeing it, so it was measured. All runs in
xiaobizh_n18_cpu, own staging root /tmp/jg-rpcdelta-gates, git archive +
docker cp (tarball md5 c6c747f2215b52e513f415ad8450cd1b, matched both ends),
__pycache__ cleared and the tree restored from the pristine copy between every
mutation (RESTORED_CLEAN printed each time), atom.__file__ asserted under the
staged root before each count, timeout -k 10 900, never piped.
Baseline tests/compass at 52439ae55: 604 passed.
| perturbation | result |
|---|---|
full revert — f80fedc09's model_runner.py restored verbatim (git archive, not reconstructed) |
604 passed |
| half revert — only the deleted clause put back, on the new wrapping | 604 passed |
comment made flatly false — "torch.cuda.empty_cache() runs anyway" |
604 passed |
| comment names the wrong mechanism — "tests the reply rather than the dispatched name" | 604 passed |
The realistic half-revert matters here in the way #176's did: the full revert and the
partial one are indistinguishable, because neither is observed at all. The defect this
commit removes can be reinstated, in whole or in part, with nothing going red.
The instrument is alive — five controls that do bite
A row of 604 passed is only a result if the harness can produce anything else.
| control | result | what fired |
|---|---|---|
`model` dropped from the class docstring |
1 failed, 603 passed | test_the_overrides_bind_no_attribute_that_could_hold_a_tensor |
self.kv_cache = None added to NonAllocatingRunner.allocate_kv_cache |
1 failed, 603 passed | same test |
RPC_SURFACE["exit"] flipped False → True |
3 failed, 602 passed | test_each_name_waits…[exit], test_every_waited_name…[exit], test_the_two_names_no_caller_waits_for… |
engine: the exit break moved inside the per-runner loop |
1 failed, 603 passed | test_the_two_names_no_caller_waits_for… |
engine: the break re-tested out is None instead of the name |
1 failed, 603 passed | same test |
The last two are the good news in this section: the comment's clause "the break is a
sibling of the per-runner loop and tests the dispatched name rather than any reply" is
the one sentence in the block that is held, because
test_the_two_names_no_caller_waits_for_and_what_replying_costs asserts
'if func_name == "exit":\n break' against async_proc.py — literal,
indentation included, so both the sibling-ness and the predicate are pinned in ATOM's
source even though the prose about them is not.
4. What the delta's claims would not notice — checked, not reasoned
The clause that remains makes three factual claims about ModelRunner.exit. I mutated
ATOM's engine so each claim went false in turn, and re-ran.
| mutation | result | reading |
|---|---|---|
destroy_dist_env() → pass # destroy_dist_env() (line count preserved) |
604 passed | "the distributed environment is never destroyed" is held by nothing |
setattr(self, "kv_cache", None) in allocate_kv_cache |
604 passed | "this runner allocated none" is held only against the self.x = … form |
torch.cuda.empty_cache() deleted from exit |
1 failed, 603 passed | confound — see below |
del self.forward_vars / del self._fv_ring added to exit |
1 failed, 603 passed | confound |
the five-name loop de-hasattr-guarded |
1 failed, 603 passed | confound |
The confound is named rather than counted as coverage. All three red rows failed the
same test — tests/compass/test_sync_inventory.py::test_recorded_lines_match_the_tree
— which is a line-drift guard over atom/compass/audit/sync_sites.json. Those three
mutations each change the line count of atom/model_engine/model_runner.py, so the
guard fires on moved line numbers, not on the claim going false. The
destroy_dist_env mutation is the honest one precisely because it preserves the line
count, and it is green. So: of the three claims, none is actually held by a test; one
looked held and was not. This is the same shape as #164's element-size 2 — three
distinct defects collapsing onto one observable.
The setattr row is worth stating separately because it is a limitation of a pin inside
the approved range, surfaced only because the delta's own clause depends on it.
_self_assigned walks ast.Assign with an ast.Attribute target, so
self.kv_cache = None fails the test (measured above, 1 failed) and
setattr(self, "kv_cache", None) does not (604 passed). Naming it, not re-opening it.
What all of this does not say: none of it is an argument against the commit. The
commit removes a false clause; an unpinned true sentence is strictly better than an
unpinned false one. It is the standing risk that this exact sub-clause has now carried a
wrong or unsupported consequence at three heads and been corrected three times, with no
instrument at any of them.
5. Gate — re-measured, not carried
Node 18, xiaobizh_n18_cpu. Each tree staged fresh by git archive + docker cp into
my own root, never written into the shared mount, gated with its own
scripts/compass/gate_cpu.sh, PYTHONPATH at its own root, atom.__file__ printed and
asserted under that root before any count, timeout -k 10 2400, output captured to a
file and read from the file — never piped.
| tree | stamp | atom.__file__ |
result | rc |
|---|---|---|---|---|
| control | 92f1fdafe |
/tmp/jg-rpcdelta-gates/control/ATOM/atom/__init__.py |
4557 passed, 149 skipped, 3 xfailed, 42.12s | GATE_CPU_RC=0 |
| prev (cycle-3 head) | f80fedc09 |
…/prev/ATOM/atom/__init__.py |
4560 passed, 149 skipped, 3 xfailed, 37.87s | GATE_CPU_RC=0 |
| branch (this head) | 52439ae55 |
…/branch/ATOM/atom/__init__.py |
4560 passed, 149 skipped, 3 xfailed, 40.02s | GATE_CPU_RC=0 |
gate: 29 files excluded + tests/plugin and gpu: not required (.compass-changed stamp) on all three. Every run printed its GATE_CPU_RC line, so none of them hung.
Gate delta of the commit under review: zero. --collect-only over tests/compass
at f80fedc09 and 52439ae55 is identical, node id for node id (604 each).
Against the control it is +3, and they are the same three cycle 3 named:
+ test_runner_non_allocating.py::test_the_docstring_names_every_attribute_that_holds_the_ring
+ test_runner_non_allocating.py::test_the_overrides_bind_no_attribute_that_could_hold_a_tensor
+ test_runner_non_allocating.py::test_what_that_ring_costs_is_the_batch_budget_by_the_hidden_size
TestTheRegionIsNotCopiedPerChunk does not appear in any of the three logs — no
failure to check against the three-way flake.
Lint, with one correction to cycle 3's figures. ruff 0.16.7: RUFF_CHECK_RC=0 at
both heads. ruff format --check atom/compass tests/compass returns rc=1 at both
heads identically — 7 files would be reformatted, 56 already formatted, headed by
atom/compass/audit/sync_scan.py:437, which is the known pre-existing baseline and
predates this branch. Cycle 3's RUFF_FORMAT_RC=0 was presumably over the narrower
per-file glob; on that glob I reproduce it —
ruff format --check atom/compass/runner/model_runner.py → 1 file already formatted,
rc=0. black --check (the tree's actual formatter, black 26.5.1) over
atom/compass/runner plus both runner test files: 6 files would be left unchanged,
rc=0. Flagging the difference rather than quoting the narrow number, since a
package-wide glob is what a merge will see.
AI_DEV_RULES.md mechanical check: grepping every + line of
git diff f80fedc09 52439ae55 for design-doc paths, NN_*.md filenames, D<n>/T<n>
labels and "principle N" returns nothing. The comment's only citations are engine
source lines (async_proc.py:166, :167), which is the allowed form.
6. Size — my own count
Counter reproduces landed atom/compass/spec/ as 275 / 495 / 763 under both
black 26.5.1 and ruff format 0.16.7 — the two formatters give byte-identical counts
on this package, so the 469-vs-495 column disagreement does not arise here and both
columns are quotable.
| unit | statements | code | physical |
|---|---|---|---|
delta model_runner.py f80fedc09 → 52439ae55 |
11 → 11 (+0) | 22 → 22 (+0) | 93 → 92 (−1) |
whole PR, production model_runner.py 92f1fdafe → 52439ae55 |
11 → 11 (+0) | 22 → 22 (+0) | 76 → 92 (+16) |
whole PR, test test_runner_non_allocating.py |
134 → 163 (+29) | 162 → 203 (+41) | 326 → 427 (+101) |
tests/compass/test_runner_rpc_surface.py (untouched, for contrast) |
264 → 264 | 422 → 422 | 721 → 721 |
numstat cross-check on the physical column, which is the only column numstat witnesses:
93 + 6 − 7 = 92 and 76 + 24 − 8 = 92 and 326 + 101 − 0 = 427, all exact.
Two of cycle 3's figures move by exactly this commit and should be corrected in the body
at merge: production physical is +16, not +17, and the PR's numstat for
model_runner.py is 24 8, not 25 8. Statements and code are +0 at both heads, so
+29 test statements remains the whole of the size for #186 and is still inside the
10–30 estimate.
7. Does anything here disturb #208? No — and it is already on top of it.
Read-only, via the API; I did not touch the PR. compare/52439ae55...43e0d5f58 (#208's
head) returns status: ahead, ahead_by: 2, behind_by: 0. #208 already contains
this delta commit; there is no restack to do and nothing to retarget. Its two commits
touch atom/compass/runner/overrides.py and tests/compass/test_runner_rpc_surface.py
— disjoint from model_runner.py, so no textual interaction either. Semantically the
two agree rather than collide: #208 pins the exit break, which is the one clause of
this comment block that my perturbations found already held.
8. Two observations for the owner — neither is a change request
- This comment block is unpinned, and the clause in it has been wrong three times.
Four perturbations of it, including a flatly false one and a full revert, all leave
604 passed. I am not asking for a pin: a#comment is not a natural pin target
and demanding one here would be the scope creep CompassModelRunner's docstring says it holds no named tensor; it holds two #186 was cut to avoid. But the record
should say that the fix landing in this commit is protected by nothing, so the fourth
drift will look exactly like the first three. test_the_overrides_bind_no_attribute_that_could_hold_a_tensordoes not see a
setattrbinding (setattr(self, "kv_cache", None)→ 604 passed; theself.x = …
form → 1 failed). That pin is inside the cycle-3 approved range and I am not
re-opening it; it is recorded because the delta's own clause "this runner allocated
none" rests on it.
Delta review by an agent, pinned to head 52439ae55. Range f80fedc09..52439ae55,
gated against base 92f1fdafe28817f864261f632d98cf5b052016dd. Verdict:
APPROVE. Production byte-identical at the AST and token level; gate delta zero
(604 → 604, collection identical); no pin in the delta, measured four ways with five
live controls; #208 already contains this commit.
Closes #186.
A docstring in landed code said something the code contradicts. This corrects it,
pins it so a successor cannot drift the same way, and reports an audit of every
other prose claim in the same file.
Principle 8 is the whole of why this exists — a claim without its measurement
is a defect, and this one had drifted away from the measurement it described.
Principle 7 shapes the fix: the paragraph now decomposes the residue instead
of naming only the dominant term.
Cycles 1 and 2 were REQUEST_CHANGES and cycle 3 APPROVED with one deletion;
everything raised is addressed at
52439ae55. What changed, and what thefindings turned up, is sections 8 to 10.
1. The sentence was false — verified here, not quoted
atom/compass/runner/model_runner.py's class docstring ended:Re-verified at
92f1fdafewithast(the module cannot be imported without adriver), reading ATOM's own
atom/model_engine/model_runner.py:self.forward_varsmodel_runner.py:1297allocate_forward_varsbuilds: 5CpuGpuBuffers, theinput_idsbuffer, and theoutputstensorself._fv_ringmodel_runner.py:1358,:1383pp_size <= 1,pp_sizeslots otherwiseBoth are named attributes of the runner, and they hold that memory for the life
of the process. Exactly the two the issue names, at exactly the three lines. The
sentence was false on its plain reading; it was true only under "a tensor bound
directly to an attribute of the subclass", which is an instrument's definition
and not something a docstring can carry.
The measurement it overstates is right and is untouched. #76 recorded 0 bytes
allocated, 0 reserved and 0 operators dispatched across
_build_and_load_model+_maybe_warmup+allocate_kv_cache(2048), against a base that would read1,503,300,328 B (Qwen3-0.6B) or 55,563,006,776 B (Qwen3.8-27B) of checkpoint.
What is wrong is "holds nothing". Construction leaves resident, measured on
xiaobizh_n18at TP1, single process, Gloo rendezvous,enforce_eager=True,load_dummy="empty",gpu_memory_utilization=0.10, Qwen3-0.6B,hidden_size=1024, bf16 (#76's closing handoff):__init__forward_varsoutputstermSo 16.9 MiB is one hidden size, and the claim that survives is
O(batch budget x hidden size). The docstring now states that shape and leaves the
bytes here — for a better reason than the one this PR first gave. #80's round 3
ruled a docstring cannot carry a measurement's conditions, which only says where a
number may not go. The stronger reason is that a shape is a structural
property of the source and can be pinned where it is asserted, which test 2
below does by reading
torch.empty(self.max_num_batched_tokens, ..., hidden_size)off the AST in this same commit. The bytes can get that on no CPU tier at all.
2. A second, smaller defect in the same sentence-pair
reads as all of the residue. The ring was measured at 99.1% / 99.4% of it;
the remaining 0.6-0.9% is a CUDA stream, the ring's events, the attention
metadata builder and the expert-load-balancing runtime. Corrected to "almost all
of what stays resident", with the remainder named — principle 7 again.
The corrected paragraph also states one fact the old one did not: the dominant
term is allocated once however deep the pipeline is.
_clone_slot(
model_runner.py:1372-1381) clones onlyCpuGpuBuffers, so a ring slot sharesthe
outputstensor and copies only the staging buffers.3. It is pinned — three CPU-only tests
In
tests/compass/test_runner_non_allocating.py, which already reads ATOM'ssource with
astfor exactly this reason (importing the binding module runsaiter's
rocminfoprobe and raises where there is no driver):test_the_docstring_names_every_attribute_that_holds_the_ring— over thewhole
ModelRunnerbody, 94self.x = ...bindings, the ones whose valuebuilds or holds a ring buffer are exactly
{forward_vars, _fv_ring}, andthe class docstring names both.
test_what_that_ring_costs_is_the_batch_budget_by_the_hidden_size— theoutputsentry istorch.empty(self.max_num_batched_tokens, ..., hidden_size).The bytes are a measurement and stay in the record; which two numbers they are
a product of is a structural fact, so it is pinned.
test_the_overrides_bind_no_attribute_that_could_hold_a_tensor— the strongerhalf, and the half this package owns:
NonAllocatingRunnerbinds onlymodeland
_token_stream, so the subclass adds no tensor of its own. It mirrors test1's last two lines, holding the class docstring's enumeration to the same two
names, so prose and source fail together in both directions.
The pin bites — measured, not asserted. Three perturbations, each run against
the test's own code:
self._fv_spare = CpuGpuBuffer(1)in_init_forward_vars_ring){_fv_ring, _fv_spare, forward_vars}— test 1 fails__init__(self._fv_spare2 = CpuGpuBuffer(1)){_fv_ring, _fv_spare2, forward_vars}— test 1 failsTest 1 therefore fails both when a named tensor appears on the runner and when
the corrected sentence stops being true, which is what #186 asked for. Test 3's
mirror was measured the same way, after cycle 2 found the pin one-directional:
_token_streamdropped from the class docstringself._scratch = 1added toNonAllocatingRunnerTwo nearby bindings are deliberately outside it, and the test says so rather
than leaving a successor to wonder.
self.forward_varsis assigned a second timeat
:1405, where_advance_forward_varsrebinds the name to a slot of the ringit already holds — a rotation, not a fourth holder. And
self.tokenID_processor.input_ids(:1408) is a fourth name reaching a ringbuffer, one attribute deeper, which is true and correctly outside a claim about
attributes on the runner.
4. The audit of
model_runner.py— 24 claims checked, 21 true#104 corrected three prose claims in this area and missed this one, so the sweep
is the point rather than a courtesy. Every checkable prose assertion in
atom/compass/runner/model_runner.pyat92f1fdafe, checked against source:runner_qualnamenames"92f1fdafeagainst merge-base0b4f1ddbatouches only.gitignoreandCLAUDE.mdoutside the compass dirstest_the_replaced_methods_...overridesholds the bodies and says whymax_num_batched_tokensbyhidden_sizeNonAllocatingRunnerfirst, so its methods win[CompassModelRunner, NonAllocatingRunner, ModelRunner, object]__init__; the base runs all of its own firstgetattr(runner, name, None)and skips Noneasync_proc.py:237-239continuelogs nothing; 90 s park measured in #76RPC_SURFACEhas 12 entriescall_funcdoesoutputs_queue.get()with no timeout,async_proc.py:431busy_loopskips it and carries onexitmeans the loop never breaks"process_kvconnector_output, a KV load is silently never startedengine_core.py:500does not waitAsyncIOProc.__init__resolves the runner class at:166beforeself.runners = []at:167AttributeError: ... no attribute 'runners'init_exit_handlerregistersweakref.finalize(self, self.exit)(atom/utils/__init__.py:573) before:166, andAsyncIOProc.exititeratesself.runners(:176)21 of 24 true. Three not, and two of those three are the sentence-pair #186
names. The third, #19, is new and is corrected in the same file:
busy_loop'sif func_name == "exit": break(async_proc.py:251-252) is indented 12 spacesagainst the
for runner in self.runners:body's 16, so it is a sibling ofthat loop and fires on the name dequeued at
:234regardless of any reply. Whata hole in
exitactually loses isModelRunner.exit(model_runner.py:1038):the distributed environment is never destroyed, and the graphs and the five KV
tensors it deletes stay held. It is in this PR's file set, so it is fixed here
rather than handed to a PR that might not take it, which is how #186 itself
survived.
What this audit covered, precisely: one file. It is not a statement about the
package. The review found the un-partitioned twin of claim #19 two files away —
atom/compass/runner/__init__.py's package docstring says "a hole in that surfaceis a caller that waits forever rather than an error", which is false for the two
Falseentries inRPC_SURFACE. That is outside this file set, so it isissue #193 with a named result, not a fold-in and not a hand-off to whatever
edits that file next.
atom/compass/runner/__init__.pyandoverrides.pyhavenot been audited.
5. Gates
1. ATOM's suite unmodified, as a delta against a control measured in this
session. Both sides staged by
git archive+ stamps +docker cpintoxiaobizh_n18_cpu(md5 verified on both ends), each run withPYTHONPATHset toits own root and
import atomconfirmed to resolve there, each tree's ownscripts/compass/gate_cpu.sh, one at a time, unpiped. Re-run at the reviewhead, not carried:
92f1fdafeGATE_CPU_RC=052439ae55GATE_CPU_RC=0Delta +3, exactly the three new tests. Skips and xfails unmoved. Both
gpu: not required (.compass-changed stamp)— neither changed path is ingpu_gate_triggers.txt. The control reproduces the 4557 this wave has beenquoting, measured rather than carried.
TestTheRegionIsNotCopiedPerChunkdid notfire on either side, at any of the four heads.
Lint, re-read at this head:
ruff checkon the two changed filesRUFF_CHECK_RC=0,ruff format --checkRUFF_FORMAT_RC=0, andruff check atom/compass/ tests/compass/RUFF_COMPASS_RC=0. ruff 0.16.7, run inthe same container on the staged branch tree at
52439ae55.2. New CPU-only tests — the three above, in
tests/compass/, no driver.3. The named result — section 1: the corrected sentence, which named tensors
the runner holds, what they cost as a function of batch budget and hidden size,
beside the measurement it describes.
4. Review — cycles 1 and 2 REQUEST_CHANGES, cycle 3 APPROVE with one
deletion. Sections 8 to 10.
6. Size — corrected, and how the wrong number happened
Counted with an
aststatement counter calibrated against landedatom/compass/spec/= 275 statements / 495 code / 763 physical, which itreproduces exactly (
codeexcludes blanks, comments and docstring lines).At
52439ae55:atom/compass/runner/model_runner.pytests/compass/test_runner_non_allocating.pyCross-checked against
git diff --numstat 92f1fdafe:24 8on the productionfile (net +16) and
101 0on the test file (+101). That check is narrower than"both agree", and cycle 2 was right to say so: numstat witnesses the
physical column only.
statementsandcodehave no independent source here.It happens to cover exactly the column that was wrong in cycle 1, so it addresses
the failure that occurred; the stated cause of the other two is arithmetically
consistent with it but is not independently verified.
The wrong number in cycle 1 was a figure carried across a commit, in a PR about
figures carried across a commit. The count was run, then
ruff formatsplit twocall expressions across three lines each, and the count was never re-run — so the
body reported 199/400 where the head read 203/404. The reviewer's
git diff --numstatcaught it as78 0against a claimed +74. The fix is not a bettercounter: it is that the counter now runs after the formatter, and the numstat
cross-check is part of the measurement rather than a thing the reviewer does.
The production change is prose, not zero code: the docstring is one
Exprnode before and after, so the statement count cannot move. +29 test statements,
at the top of the 10-30 estimate.
7. What a successor should know
astread of ATOM's source, so it is exact-string onCpuGpuBuffer,torch.emptyandself.forward_vars. A buffer built through anew helper name would not be seen. That is the same trade
test_only_the_binding_module_reaches_the_enginealready takes in this file,and it errs toward firing on a rename rather than staying silent. What it does
now cover is the whole class, so where a holder is bound no longer matters.
CUDA allocator on a machine with a card. The tests pin the shape, which is a
structural property of the source; the product itself stays a measurement in
this record.
_fv_ringatpp_size > 1was never measured — every figure above is asingle-slot ring. The per-slot clones copy the staging buffers, which are the
1.5-3.9% terms (the gap between
forward_varsand itsoutputsentry), not thedominant one.
atom/compass/runner/__init__.pyandoverrides.pycarry unaudited prose.compass: the runner package docstring names the wrong failure for the two unwaited RPC names #193 covers the one false claim already found in the first of them.
successor quoting statements or code from this record is quoting one
instrument, not two.
8. Review cycle 1 — what changed at
5b6157033F1. The walk covered two method bodies while the prose claimed the class. The
reviewer's perturbation —
self._fv_spare2 = CpuGpuBuffer(1)in__init__— leftthe test green. The walk now reads the whole
ModelRunnerbody; over 94self.x = ...bindings the answer is the same{forward_vars, _fv_ring}, whichis the reviewer's own independent pass, so widening changed only what the test can
see. The test docstring and section 7 no longer overstate it.
Widening turned up a defect the narrow walk structurally could not show. The
helper kept one value per name, and
forward_varsis assigned twice — the dictof buffers at
:1297and the rotation rebind at:1405.ast.walkdoes not visitin source order, so the rebind displaced the dict and
forward_varsdropped outof the holder set entirely: the widened test failed its own unperturbed tree.
The helper now returns
(name, value)pairs and a name counts as a holder whenany of its bindings does. Worth stating plainly: the narrow walk was passing partly
because it never saw the second assignment, which is the same shape of luck the
original docstring was relying on.
F2. The new paragraph dropped
_token_stream. The list was carried verbatimfrom #76's handoff rather than re-derived, while this branch's own test 3 asserts
NonAllocatingRunnerbinds exactlymodeland_token_stream(
overrides.py:382). The docstring now names both, and separatesconfig.num_kvcache_blocksas a count rather than a buffer. The two lists are nowpinned against each other.
F3. Section 6, with the mechanism.
Also folded in:
gpu_memory_utilization=0.10restored to #76's stated conditions;the
exitcorrection now names what is lost and why the loop still breaks; row 3'sevidence is now about the landed tree rather than this diff; #193 opened.
9. Review cycle 2 — what changed at
f80fedc09F1. The pin was one-directional and a sentence said otherwise. Test 3's
docstring claimed the class docstring was held to the same two names it asserts.
Nothing asserted that: dropping
_token_streamfrom the prose, or replacing thewhole enumeration with one bare sentence, both left the suite green. This was a
cycle-1 request declined by writing a sentence that said it was done — which is
worse than declining, because from outside an unread finding and a rejected one
look the same. It is stated here rather than quietly fixed.
The fix taken is the mirror, not the deletion, of the two the reviewer called
equally acceptable. Deleting the sentence would leave the enumeration that cycle 1
corrected as another unpinned prose claim, which is the exact defect this branch
exists to fix; the sentence describes the property worth having, so the property
is what changed. Both perturbations that were green are now red, and the third
stays red.
N1. The helper docstring named the wrong mechanism. It blamed
ast.walknotvisiting in source order. That is true in general and is not what fired: the
two
forward_varsbindings are walked in source order, and what discarded thedict of buffers was last-wins in a mapping keyed by name, keeping the rotation
rebind, which names no buffer. The sentence now says that and makes no claim about
walk order at all. A correct explanation of the wrong mechanism is the class of
defect this PR is about, so it does not get to sit in the fix for it.
N2. The
exitcomment overstated in the other direction. It said the graphsand five KV tensors stay held; on this runner neither exists, because
allocate_kv_cacheallocates nothing and the deletions atmodel_runner.py:1051-1059arehasattr-guarded. What an unansweredexitactually loses here is
destroy_dist_env()(:1044),del self.model(
:1060-1061) andtorch.cuda.empty_cache()(:1064). The comment names thosethree — and, after cycle 3, stops there; see section 10.
10. Review cycle 3 — APPROVED, with one deletion at
52439ae55The rewrite above was line-exact on every citation and then attached a
consequence that does not exist: "... and
torch.cuda.empty_cache()neverruns, so the forward-vars ring stays in the allocator." Verified here rather
than taken on the review's word: no
del self.forward_vars,del self._fv_ring,= Noneordelattrfor either name exists anywhere inatom/model_engine/model_runner.py, soexitnever releases the ring; andtorch.cuda.empty_cache()releases "all unoccupied cached memory" — read fromthe torch in the gate container — which a live attribute of a live runner is not.
The ring's residency is invariant to whether
exitruns. Of the three lossesnamed, only
destroy_dist_env()has a real cost here;self.modelis anUnbuiltModelregistering no parameter and no buffer.The pattern is worth more than the clause, and it is the best thing this review
found. This one sub-clause carried a consequence it does not have at three
consecutive heads:
92f1fdafe(landed)5b6157033f80fedc09exitdid runEach rewrite fixed the last and reached for a new consequence, because a true
observation feels like it must lead somewhere. It does not. The sentence now
ends at the fact. That failure mode — a claim repeatedly repaired by
substituting a different wrong consequence — is carried into the handoff.
🤖 Generated with Claude Code