Skip to content

test: record A once, replay B against it, as one rank or several - #20

Merged
xiaguan merged 18 commits into
masterfrom
multi-rank
Sep 15, 2026
Merged

xiaguan merged 18 commits into
masterfrom
multi-rank

Conversation

@xiaguan

@xiaguan xiaguan commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

What

kern test no longer loads A and B side by side. It records A (state image per program run, frontier inputs and reference outputs per span, all kept on the device; A's noise floor, fuzz outputs and timings), drops A, loads B and replays: B starts every run from A's image and every span from A's inputs, then free-runs the workload for the end-to-end logits. The two sides are never co-resident, so a model that fills its GPUs (DSv4.1 Flash EP4, 119 GB of weights per rank) can be tested at the price of loading twice.

A side may be several ranks. Side names the rank on everything that moves bytes and runs everything on all ranks at once (a collective inside a span needs its peers issuing). Recording and comparison are rank-local; findings say rank q. In kern-run the side is Ranks: one runtime per GPU with its place in the topology, {ep} weight shards, peers exported/imported, once programs run, one thread per rank for run/time/capture, the slowest rank's time reported, a hung rank named after 600 s. --gpu takes a list, or one ordinal to start the ranks at.

In the static diff a peer buffer stands for the exported buffer or state it holds the group's addresses of (the kernel given every rank's copy reads and writes this rank's target too), so the target is on the span's frontier and the address array is not.

The end-to-end oracle is KL, not ulps (commit 17f9604)

The logits limit used to be counted in ulps of the row's storage dtype; DSv4.1 keeps its logits in f32 over an fp8/bf16 pipeline, so the real run read 1.7e7 ulp against a limit of 4 and decided nothing. Every row is now measured on the distribution: KL(A‖B) in nats against --logit-kl (default 0.01, kern.toml [test] logit_kl). A flip within the limit is a tie that broke the other way; a flip beyond it is the FAIL (NaN on B's side included). The old near-tie rule (A's margin ≤ B's Δ) is gone: it excused flips by the size of B's own error. The report follows A's top-20 through B (overlap, A's token's rank in B) as a diagnostic, not a gate, and noise runs A's workload once more to report A against itself end to end. --logit-ulp is deleted.

Tests

  • cargo test -p kern-test: 22 harness fixtures (record→replay, a two-rank fixture whose findings all say rank 1, a rank-count mismatch, declared-differently / once / logits-name rules; the KL verdict on rounding, drift, wide flip and NaN), peer-buffer and once-constant diff tests, compare/workload property tests (logit row: KL, top overlap, rank in B).
  • cargo clippy --all-targets -D warnings, cargo fmt clean.
  • Real run on a GB300 tray with DSv4.1 Flash EP4 (A = paged attention, B = fused decode): see the follow-up comment.

Docs

docs/test.md: the design section now describes record → replay and a new 多 rank chapter; docs/roadmap.md updated.

Not to be merged yet (per request).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-15T06:23:00.014696Z 7c1c58a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c1c58a681

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/kern-run/src/test.rs Outdated
Comment on lines +313 to +314
fn bytes(&self, from: &Scratch, len: usize) -> Result<Vec<u8>> {
Ok(self.rt.read_scratch(from, len)?)
Ok(self.rt(0).read_scratch(from, len)?)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Read scratch buffers through their owning rank

For any multi-rank run, snapshots allocated with rt(rank).scratch(...) on ranks above zero are still read through rank 0's runtime here. Runtime::read_scratch binds that runtime's CUDA context and uses its stream, so default fuzzing will attempt to read rank 1+ device pointers from GPU 0; the final-state comparison does the same even when fuzzing is disabled. Pass the rank through Side::bytes and dispatch to rt(rank).

Useful? React with 👍 / 👎.

Comment thread crates/kern-test/src/harness.rs Outdated
} else {
findings.push(Finding::of(&run.program, &label, n, &c));
}
res.entry(run.program.clone()).or_default().entry(n.clone()).or_default().push((label.clone(), c));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve rank identity in noise-floor keys

In a multi-rank run where A's nondeterminism differs by rank, this stores every rank's noise under the same program/buffer key (and the state path similarly omits the rank). The later within_noise calculation compares global maxima and membership using those collapsed keys, so noise observed only on rank 0 can authorize an unrelated B mismatch on rank 1 and produce a false PASS when fuzz/logit evidence does not take precedence. Include the rank in both noise and local matching keys.

Useful? React with 👍 / 👎.

@xiaguan

xiaguan commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Real run: DSv4.1 Flash EP4, paged → fused, 4 ranks on one GB300 tray (2026-09-15)

A = a-paged, B = b-fused (fused decode, fp8 attention_raw), --capacity 512 --prefill 64 --chunk 64 --decode-steps 8 --no-sweep. 20 ops swapped; 120–129 spans per program.

stage time
load A (4 ranks) 34 s
record A (7 runs, 369 spans kept, noise + fuzz + perf) 364 s
load B 34 s
replay B (tap 54 s, noise 13 s, fuzz 288 s, perf 3 s) ~400 s
total 797 s
  • end to end: next_token bit-identical on all four ranks; verify_tokens 5/6 differ (the draft path diverges), nacc differs by 1.
  • logits: 4036 rows, 148 differ, 76 argmax flips, all near-ties (A's margin < Δ). Target rows: 16 differ, max |Δ| 5.6, the one flip is prefill chunk 0 (margin 0.19, Δ 2.6). Draft rows max Δ 4.1; verify rows up to 16 but they follow drafts that already differed.
  • local: 0/2312 bit-identical: B's q is the fused-prep layout under the same name; attention_raw (bf16 → fp8) and friends are excluded as declared differently.
  • noise: A itself is not deterministic at 492/984 spans (o_lowrank 50 % of elements, 16k ulp).
  • perf: decode_batch eager 17.7 → 15.0 ms (−15.6 %), graph TPOT 11.41 → 10.57 ms (88 → 95 tok/s), spans −46.6 %; round TPOT 12.97 → 11.93 ms; prefill 64 rows 22.4 → 18.9 ms; fused attention 281 MB/call at 7.5 TB/s (93 % of peak).
  • verdict: INCONCLUSIVE (logits move past the limit, no wide flip, A non-deterministic). Read the margins: every flip is a near-tie and decode tokens agree.

Fixed on the way, each with a fixture: once-program outputs are load-time constants, not span inputs (200 GB of rope-table scratch otherwise); a buffer declared differently on the two sides is not compared; a changed once program is not an "untapped program"; the logits oracle matches *.logits; scratch reads name their rank.

Follow-ups in docs/roadmap.md: state images copy the block-rounded allocation (84 GB here for 12 MB live); the f32-logits ulp scale (1.7e7 ulp) is meaningless on an fp8 pipeline; the diff section prints 211 lines for a swap this size.

Archive: ~/bench_results/2026-09-15-kern-test-multirank/ (log, --out JSON, patched manifests, README).

@xiaguan

xiaguan commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

17f9604 — the logits oracle is KL per row. Recomputed from the tray18 run's archived rows (ab-report.json, 148 differing rows of 4036):

rows KL flips
before the first draft divergence (prefill, step 0, step 1 draft) 0.09 – 0.33 2, margins 0.19 / 0.22
step 5 verify (after B's draft diverged) 4.4 – 12.2 margin up to 2.69, previously excused as near-tie under Δ 14.5

Under the new rule that run reads: FAIL at step 5 verify.target_head.logits[2] (KL 4.4 above the limit), unless A's own band says otherwise — which it likely does here, since A is not deterministic at 492/984 spans. Rows after a diverged draft are not comparable under any metric; the honest fix (teacher forcing of integer decisions: draft tokens, indexer windows, MoE routing) is in docs/roadmap.md.

🤖 Generated with Claude Code

@xiaguan

xiaguan commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

104a3c1 — the FAIL on a flip beyond the KL limit is now guarded by A's own end-to-end band (noise runs A's workload twice: KL within the limit, no flips), not by the span-level noise floor. A reference that is noisy at some span but reproduces its distribution can be failed; without a noise pass the guard stays open as before. 23 harness fixtures, CI green.

@xiaguan

xiaguan commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

比较下设备 + KL 判据:DSv4.1 Flash EP4 复跑(tray18,2026-09-15)

5cf282acompare / changed_blocks / logit_stats 做成三个 kernel(compare.cu → 打包 PTX,同 profile.ptx),0c489cb 让 harness 的每一次比较都走 Side::{compare, changed, logits},判定需要的东西不再以字节形式过总线。GPU 上对 host 定义的属性测试 crates/kern-run/tests/device_compare.rs--ignored)三条全过:11 种 dtype × {Buffer, Scratch} 操作数逐计数相等,changed blocks 在 1 / 63 / 64 / 65 / 4096 / 1000001 字节上逐区间相等,logits 行在 7 … 129280 列上 argmax / rank / top-20 / margin 相等、KL 到 1e-9。

同一对 manifest、同一 seed、同一 tray 复跑(results/ab2.log):

阶段 首跑 cac019e 本次 0c489cb
load A / B 34 / 34 s 34 / 34 s
record(workload) 364 s(3 s) 323 s(1.3 s)
tap 54 s 0.9 s
logits 0.2 s
noise 13 s 0.55 s
fuzz 288 s 7.0 s
perf 3 s 3.3 s
全程 797 s 402 s

计数和 perf 逐项与首跑一致:local 0/2312、noise 492/984 clean、fuzz 1045/5904、decode_batch TPOT 11.41 → 10.57 ms(−7.4%)、round 12.97 → 11.93、prefill 22.5 → 19.0 ms、120 spans −46.7%。

判定变了:FAIL。noise 底线 "A against itself end to end: 4036 rows · max KL 0.00e0 · 0 argmax flips",所以翻转全是 B 的:3960/4036 argmax 一致,76 次翻转全部超过 logit_kl = 0.01,第一条 rank 0 prefill chunk 0: A 271 → B 201, KL 1.27e-1 (margin 0.1879, A's token is B's #2)。step 1/3/5 的 verify 行 KL 4–12 是 draft 分叉后跟着不同 token 算的,roadmap 里的 teacher forcing 项管它。0.01 nat 对 fp8 attention 是否合适由 --logit-kl 定。

还剩的时间全在 record:323 s 里 workload 只有 1.3 s,怀疑对象是 84 GB 的 state 镜像(按整块分配尺寸拷)、每个输入一次 cuMemAlloc(约 2 万次)和逐 span 的 fan-out;下一步先分相计时。这个 PR 不合,等指示。

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi

xiaguan and others added 11 commits September 15, 2026 09:25
A and B were loaded side by side and run in lockstep, so a model that
fills its GPUs could not be tested at all: DSv4.1 Flash EP4 puts 119 GB
of weights on each rank. The harness now records A first (every program
run's state image, every span's frontier inputs and reference outputs,
kept on the device; A's noise floor, A's outputs under fuzz, A's
timings), drops A, loads B and replays: B starts every run from A's
image and every span from A's inputs, then free-runs the workload for
the end-to-end logits. The recording's device handles (`Side::Buf`)
outlive the side that made them; the two loads share the primary
context.

A side may be several ranks. `Side` names the rank on everything that
moves bytes and runs everything on all ranks at once (a collective in a
span needs its peers issuing); recording and comparison are rank-local,
findings say `rank q`. In kern-run the side is `Ranks`: one runtime per
GPU with its place in the topology, `{ep}` weight shards, peers
exported and imported, `once` programs run, a thread per rank for
run/time/capture, the slowest rank's time, a hung rank named after
600 s. `--gpu` takes a list, or one ordinal to start the ranks at.

In the static diff a `peer` buffer stands for the exported buffer or
state it holds the group's addresses of: the kernel given every rank's
copy reads and writes the target on this rank too, so the target is on
the span's frontier; the address array is a load-time constant.

The verdict is unchanged. The fake side gained ranks (`Fixture::ranks`,
a `scale_rank1_wrong` entry) and a peer fixture for the diff; the same
seed on the single-rank fixtures reports the same counts as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
…nput

A span's frontier inputs excluded weights but not what the `once`
programs make of them: packed expert weights, rope tables. On DSv4.1
Flash every attention span reads the 268 MB `rope.window.interleaved`
carry, so recording 120 spans over 9 runs on 4 ranks asked for 200 GB
of device scratch per rank and died out of memory right after loading
A. These buffers are each side's own, like the weights they come from
(B may pack them in another layout), so `constants(m, once)` names
them and the recording hands over neither.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
CI's hawk lint: seven items were pub(crate) with no use outside the
module.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
A driver error out of a four-rank recording said only
CUDA_ERROR_INVALID_VALUE. Every byte-moving call of the real side now
adds which rank and which buffer or state, and the recording adds which
span it was saving or reading. A frontier input with no live bytes at
the run's vars is skipped rather than allocated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
A scratch allocated on rank 2 read through rank 0's runtime is a
pointer from another device: CUDA_ERROR_INVALID_VALUE, first seen when
fuzz took A's tapped inputs on a four-rank recording.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
DSv4.1's fused decode keeps the name `attention_raw` but stores it as
fp8 where the paged kernel wrote bf16: same name, half the bytes, no
common layout to compare. Such a buffer is neither handed from A to B
as a span input nor compared as its output; it is listed with the
one-sided writes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
DSv4.1 writes `decode.target_head.logits`; only a name starting with
`logits` was an oracle, so the first four-rank run fell back to span
identity with next_token bit-identical on every rank and no verdict.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
…d program

DSv4.1's B packs its attention weights in `load`, so `load` has spans;
it ran at load on both sides and is nothing the workload could drive.
The first four-rank run ended INCONCLUSIVE on it alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
…rage

The logits limit was counted in ulps of the row's storage dtype. On
DSv4.1 the logits happen to be f32 while the pipeline is fp8/bf16, so a
2.6 move read as 1.7e7 ulp and `--logit-ulp 4` decided nothing; the
storage of the head's output is not the precision the model ran at.

Now every row is measured on the distribution: KL(A‖B) in nats against
`--logit-kl` (default 0.01), whatever dtype the row is kept in. A flip
within the limit is a tie that broke the other way (0.55/0.45 swapping
already costs 5e-3); a flip beyond it is the FAIL, NaN on B's side
included. The near-tie rule "A's margin ≤ B's Δ" is gone: it explained
B's flips by B's own error, so the step-5 verify flip on tray18 with
margin 2.69 passed as a tie under a Δ of 14.5. The report follows A's
top-20 through B (overlap, where A's token ranks in B) as a diagnostic,
not a gate: with a margin rule it is a subset of KL, without one the
tail's ties drown it. Noise runs A's workload once more and reports A
against itself end to end, the band any KL verdict sits in.

Recomputed on the tray18 rows: pre-divergence 0.09–0.33, step-5 verify
4–12. Rows after a diverged draft are not comparable under any metric;
teacher forcing of integer decisions is in the roadmap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
…end to end

The FAIL was guarded by the span-level noise floor: any span A could
not repeat blocked the conclusion. A reference noisy at `o_lowrank` may
still land on the same distribution every run, and that, not the
spans, is what says whether B's flip is B's. The guard is now A's own
end-to-end band from the noise pass (KL within the limit, no flips);
without a noise pass it stays open as before. The INCONCLUSIVE text
names A's band when it is the reason.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
xiaguan and others added 3 commits September 15, 2026 09:33
Three harness primitives over two byte ranges on the device — `At::{
Buffer, State, Scratch}` — each one launch and a few bytes back:
`compare` (element-wise counts as kern-test's `compare` defines them),
`changed` (one bit per 64-byte block that differs, so a state's
write-set is found against an image of it without reading either), and
`logits` (one block per row: argmax, A's top two, KL, where A's argmax
ranks in B, the overlap of the top sets). The kernels are `compare.cu`,
bundled as PTX like `profile.ptx` (CUDA 13.0, compute_80) and loaded on
first use; they reproduce the host definitions exactly for counts and
maxima (dtype decoding, ulp keys and `total_cmp` ordering included) and
to rounding for the sums. `scratch` no longer zeroes: a harness fills
it before it reads it. `read_scratch` takes a range.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
`Side` gains `compare`, `changed` and `logits`, and the harness compares
where the bytes are: A's reference outputs, its logits rows and its
outputs under fuzz stay in device scratch; B's tap, the noise floor, the
fuzz rounds and the end-to-end rows are kernel launches that return
counts. A span's state write-set is the 64-byte blocks that changed
against a device image of the state (`changed_blocks` is the definition),
so the two whole-state readbacks and the byte loop per kept span are
gone — on DSv4.1 EP4 that was some 190 GB through pageable host memory
per record. The bytes that still come to the host are the write-set
itself, the inputs fuzz perturbs, integer outputs with a domain and the
end-to-end output buffers. The fake side implements the three primitives
with the host definitions; `kern-run/tests/device_compare.rs` checks
the kernels against them on a GPU (ignored in CI).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
Same manifests, seed and tray as the first run; every count and perf
number repeats, tap 54 s → 0.9 s and fuzz 288 s → 7 s once nothing is
compared on the host. Verdict is now FAIL under logit_kl 0.01: A
reproduces itself end to end (max KL 0, no flips) and B moves prefill
chunk 0 by KL 0.127. Record still takes 323 s with a 1.3 s workload;
that cost is not yet attributed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
xiaguan and others added 2 commits September 15, 2026 12:46
…d outputs

Fuzz perturbed every kept span's float inputs under six distributions
and replayed both sides. It answered a question nobody asked: with no
float domains in the manifest (a decision), a NaN under x4 scale is a
report line, not a verdict; shuffle and resample break the tensors'
semantics (q no longer matches its KV), so the differences say nothing;
jitter repeats what the noise floor already says. Every bug an op swap
produces has another stage on it (layout and edge rows: tap; shapes:
the perf sweep; determinism: noise; time: the multi-step workload). In
two real cases and dozens of fixture runs it never changed a verdict,
and it cost five of the seven minutes of a DSv4.1 EP4 kern test: A's
side decoded, perturbed and re-encoded 21 G bf16 elements on one CPU
thread (measured 16 ns each; the state images and cuMemAllocAsync,
suspected before, are milliseconds). Out-of-distribution coverage is
another --seed of the workload: end to end, KL-judged, every row
explicable.

What stays is fuzz's post-condition: an output B writes end to end
must lie in its declared domain, or the verdict is FAIL. Side::write
and Options.fuzz go with it; the kern-test suite is 22 fixtures.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
…econds

Same manifests, seed and tray; verdict, counts and perf unchanged.
The state-image roadmap item goes: master's pool sizes states by the
capacity's tokens, so the images are the live 12 MB per rank per run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
@xiaguan

xiaguan commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

record 的 323 s 归因,fuzz 删除,第三跑 74.5 s

上一条评论里"84 GB 镜像 / 两万次 cuMemAlloc"的怀疑不成立,实测排除:cuMemAllocAsync 1 MB 5 µs、3 GB 3 µs,几千个存活也不涨;84 GB D2D 是毫秒级;workload(含 7 次 run 和全部镜像)1.3 s,noise 0.55 s,perf 3 s。余下全是 fuzz 的 A 侧:每轮把 kept span 的输入读回 host,单线程逐元素解码、扰动、编码、写回,release 下 jitter / noise 每元素 20 ns,6 轮 21 G 个 bf16 ≈ 330 s。

72c5a55 删掉 fuzz 段(Side::writeOptions.fuzz--fuzz、六种 perturb、FuzzRec / Fuzz 报告段一起),理由在 docs/test.md "没有 fuzz" 一段:没有浮点 domain 就没有判据;shuffle / resample 之后 q 与 KV 不再对应,差异不说明任何事;jitter 重复 noise floor;两个真机 case 与所有 fixture 它从未改变过判定。留下的是它的后置条件:B 端到端写出的 output 越出声明域即 FAIL(local ✗ domain: 行)。分布外覆盖 = 换 --seed 再跑,端到端、走 KL。

同 tray 第三跑(results/ab3.log):

阶段 首跑 第二跑 本次
load A / B 34 / 34 s 34 / 34 s 33.6 / 31.3 s
record 364 s 323 s 5.3 s
tap + logits + noise + perf 70 s 5 s 4.9 s
全程 797 s 402 s 74.5 s

判定、计数、perf 逐项与前两跑相同(FAIL:prefill chunk 0 A 271 → B 201,KL 0.127)。state 镜像 84 GB → 339.5 MB 是 master 的 pool 改按 capacity 分配 state 的结果(rebase 带进来的),roadmap 里那一项已删。分支已 rebase 到 master,CI 绿,仍不合。

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi

xiaguan and others added 2 commits September 15, 2026 13:28
…tion first

Review findings, each confirmed on the fixtures:

- 0/0 compared was PASS: bit-identical, ahead of the logits oracle. A B
  that declares every compared buffer differently passed no matter what
  it computed. Nothing compared is not everything identical; the oracle
  decides (new fixture).
- logits buffers, end-to-end outputs and shared states paired A with B
  by name alone while spans used dtype and shape; a B that re-declares
  its logits or repacks a state read as garbage or an opaque range error
  from a rank thread. One `alike` now decides all of them; states must
  declare the same, or they join the same report line.
- a frontier input the two sides declare differently was dropped
  without a word, so B's span read its own stale copy and the span was
  no longer span-local. It is now named on the report line with the
  writes that are not compared.
- B's tap ran spans from the run image only; docs/test.md promised A's
  pre-image before and A's post-image after on both sides. A state two
  spans of one run touch made B's second span read its own first. The
  tap now restores like the noise floor does.
- the FAIL names the flip with the largest KL, as the report line does.
- logit_rows declared 49,664 B of static shared memory, over the 48 KiB
  limit ptxas enforces for every arch (the resident driver's JIT let it
  through). 256 threads per block, 12 KiB; the PTX is regenerated and
  assembles for sm_80 and sm_100.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
Review cleanups. `Side::alloc` existed to feed `save` / `save_state`
at every one of its five call sites; the two now return the device
handle (`Runtime::save_buffer` / `save_state` likewise, `scratch` is
private). `capture` was always followed by `time_captured`: one
`time_graph`. A's noise pass and B's free run were the same loop:
one `free_run`. `logit_row`, the `Bytes` alias, the unreachable E8M0
arm of the kernel's `ulp_key`, `Logits.runs` (printed nowhere) and the
`ranges_of` re-export go. kern-run's `cmp_of` / `logit_of` are the one
place the device counts become kern-test's types, and the GPU oracle
test now goes through them instead of a copy.

The GPU test also covers what the kernel is most likely to get wrong:
one column, a row of one value everywhere on both sides (every
tie-break by index), a NaN in A, a signed zero, and `changed` over an
operand three bytes off the 16-byte alignment. 3/3 on GB300 with the
256-thread `logit_rows`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi
@xiaguan

xiaguan commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

审查修复(8b876c3bea0acf)与第四跑

对整个 diff 做了一轮独立审查(正确性 / dead code / 冗余 / 过度设计 / 测试),修了其中的全部实质项:

bug

  • local 0/0 bit-identical 曾直接 PASS(排在 logits 判据之前):B 把所有比较 buffer 声明成不同形状就无条件通过。现在 compared = 0 不算"全同",由 oracle 判;新 fixture nothing_compared_is_not_everything_identical
  • logits buffer、端到端 output、共有 state 只按名字配对,span 却用 dtype+shape:现在一个 alike 管全部,state 要求声明完全相同,否则与单侧写的 buffer 同一行报出。
  • 两侧声明不同的 frontier 输入原来静默不注入(B 的 span 读自己的旧值,不再 span-local):现在点名在同一行。
  • B 的 tap 只从 run 镜像起跑、不按 span 写回 A 的前后像,与 docs/test.md 的承诺不符(同一 run 两个 span 碰同一 state 时 B 的后一刀读到自己前一刀):现在与 noise 路径一样写回。
  • logit_rows 静态 shared memory 49,664 B 超过 48 KiB 上限,ptxas 对每个 arch 都拒编,只是当前 driver 的 JIT 放过了:改 256 线程 / 12 KiB,PTX 重生成并 ptxas -arch=sm_80/sm_100 验过。
  • FAIL 文本点名 KL 最大的翻转,与 logits 行的 max KL at 一致。

删 / 合Side::alloc 并入 save / save_state(返回句柄,Runtime 同样,scratch 私有);capture + time_capturedtime_graph;A 的 noise 自由跑与 B 的自由跑共用 free_runlogit_rowBytesulp_key 不可达分支、Logits.runsranges_of 导出删除;GPU oracle 测试改走 kern-run 的 cmp_of / logit_of 而不是复制品。

测试:GPU 属性测试补单列、全等行(全部按下标破平局)、A 侧 NaN、±0、非 16 字节对齐的 changed 操作数;3/3 过。

tray18 第四跑(results/ab4.log):74.3 s,计数与 perf 与前三跑逐项相同,FAIL 现在点名 rank 3 step 1 verify.target_head.logits[4]: A 19894 → B 15, KL 1.22e1(最大者;prefill chunk 0 那条 KL 0.127 仍在 logits 段里列着)。CI 绿,仍不合。

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y5ggq6AWjBmMxNdeAgxqVi

@xiaguan
xiaguan merged commit b325e82 into master Sep 15, 2026
3 checks passed
@xiaguan
xiaguan deleted the multi-rank branch September 15, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant