Skip to content

MegaKernels in Iris 🔥 - #541

Draft
neoblizz wants to merge 63 commits into
mainfrom
neoblizz/megakernel-perf
Draft

MegaKernels in Iris 🔥#541
neoblizz wants to merge 63 commits into
mainfrom
neoblizz/megakernel-perf

Conversation

@neoblizz

Copy link
Copy Markdown
Member

This pull request introduces the GPT-OSS-120B Megakernel example, providing a comprehensive, high-performance single-GPU implementation of the GPT-OSS-120B model using a persistent Triton kernel. The changes include detailed documentation, benchmarking and accuracy scripts, and a reusable set of device-level Triton operations for attention, quantization, and expert routing. Together, these updates enable end-to-end quantized inference, benchmarking, and accuracy evaluation for this large mixture-of-experts model.

Major additions and improvements:

1. Documentation and Usage:

  • Added a detailed README.md describing the GPT-OSS-120B Megakernel architecture, model details, quantization options, benchmarking methodology, accuracy tradeoffs, and file purposes.

2. Benchmarking and Evaluation Scripts:

  • Introduced bench_tpot.py for measuring steady-state decode latency (TPOT) and comparing quantized vs. BF16 inference paths.
  • Added bench_islosl.py to benchmark prefill and decode latency across various input/output sequence lengths, reporting throughput and latency metrics.
  • Implemented acc_eval.py to compare accuracy between FP8 and BF16 attention (with shared FP4 experts), providing detailed metrics such as top-1 agreement, top-k overlap, KL divergence, and logit cosine similarity.

3. Reusable Device Operations:

  • Created common/__init__.py to expose a suite of reusable Triton device ops for attention, quantization, GEMV, routing, and SwiGLU, supporting both single-GPU and future multi-GPU kernels.
  • Added common/attention.py containing Triton JIT helpers for RoPE+KV cache appending and per-head flash decode with attention sinks, facilitating both current and future kernel designs.

neoblizz added 30 commits June 22, 2026 13:40
Single persistent Triton kernel running attention and MoE for all 36
layers of GPT-OSS-120B on one GPU (batch-1 decode), collapsing cosmic's
multi-GPU design. FP4 expert weights dequantized in-kernel, BF16 compute.
Validated against a PyTorch reference: greedy decode produces coherent
output on real weights. Includes HF->.iris converter and kernel tests.
Expert GEMVs can now run native FP4xFP8 scaled matrix multiply via
tl.dot_scaled (lowers to v_mfma_scale_f32_16x16x128_f8f6f4 on gfx950)
with dynamic FP8-E4M3 activation quant, selected by --quant. Keeps the
BF16 dequant path as default. Quantized path is ~2.8x faster (29ms vs
84ms TPOT on MI355X) and still decodes coherently. Adds dot_scaled and
quantized-expert correctness tests plus a TPOT benchmark.
Block-of-rows GEMV tiling with max_contiguous/multiple_of hints so the
compiler emits dwordx4 loads instead of per-element ushort. Fuse the
attention RMSNorm into QKV and the MoE RMSNorm into the router GEMV, fold
RoPE into attention, and stripe the residual/zeroing across all programs.
This removes the serial single-program phases and their grid barriers.
TPOT drops from 29.4 to 25.3 ms (quant) on MI355X; output unchanged.
In the quantized path, each program now owns whole 32-element SwiGLU
output blocks and quantizes the block it just produced, so the producer
and consumer are the same program and the separate act-quant barrier is
gone (4 fewer grid barriers per layer). TPOT drops from 25.3 to 22.9 ms
(quant) on MI355X; output unchanged.
Document the measured findings (zero spills, per-element loads, barrier
cost) and the tiling + barrier-reduction passes. Refresh the TPOT table:
quantized 29.4 -> 22.9 ms on MI355X.
Simplify the README to match the other examples, rewrite the module and
test docstrings for clarity, and remove implementation-history detail.
No functional changes; both decode paths still produce the same output.
The top-k experts are independent until the final accumulation, so run
each expert phase (gate-up, SwiGLU, down) across all experts before the
next barrier instead of one expert at a time. This drops the expert
barriers from three per expert to three per layer. TPOT 22.7 -> 19.3 ms.
The scaled FP4 x FP8 expert GEMV used the weight as the dot operand whose
contiguous dimension was not the inner one, and small block sizes left most
programs idle. Make the weight the lhs so its contiguous K bytes coalesce,
and use BLOCK_N=32, BLOCK_K=512, which raises the expert GEMV from ~0.6 to
~1.3 TB/s. TPOT 19.1 -> 11.7 ms.
BLOCK_M=16 lifts the BF16 tiled GEMV toward 5 TB/s on the LM-head shape.
Keep the attention output in its own buffer and defer the residual add to
the layer's final accumulation, so the router and expert input read
rmsnorm(x + o) directly. Removes the separate residual phase and its grid
barrier, and the final residual is striped across all programs. TPOT
11.8 -> 11.5 ms.
Track a running (max, index) while computing the LM-head logits instead of
writing all vocabulary logits to HBM and reading them back for the argmax.
Removes a full logits round-trip and one barrier. TPOT 11.5 -> 11.2 ms.
A 1024-wide K block on the scaled FP4 x FP8 expert GEMV raises its
bandwidth from ~1.3 to ~1.8 TB/s. TPOT 11.2 -> 10.6 ms.
The down projection (N=H) reaches best bandwidth at BLOCK_N=16 while the
gate-up (N=2*I) prefers 32, so give them independent tile sizes.
Replace the per-position scalar KV scan with a blocked online-softmax over
BLOCK_T positions at a time. This removes the linear per-token slowdown as
the context grows, so decode TPOT stays flat. At a 100-token context TPOT
drops from ~14 to ~9.5 ms; full-rollout TPOT is ~9.4 ms.
The kernel is bound by the grid-wide barriers between phases, not by GEMV
occupancy. Dropping from 256 to 192 programs makes each barrier cheaper
while the per-phase GEMVs stay well filled. TPOT 9.4 -> 7.4 ms.
Compute the current position's attention term in-register from the freshly
projected k/v and read only the history from the cache, so the cache append
and the attention can share a phase. Removes one grid barrier per layer.
TPOT 7.4 -> 7.25 ms.
Run the router GEMV, the FP8 expert-input quantization and the accumulator
reset in one phase (all depend only on x + o), then compute the top-k
redundantly per program so the experts need no separate top-k barrier.
TPOT 7.25 -> 7.15 ms.
The last expert's down projection owns the same MoE-output rows it just
wrote, so it finalizes x += o + moe in its epilogue instead of a separate
striped residual phase. Removes one more grid barrier per layer (quant
path). TPOT 7.15 -> 6.97 ms.
Smaller output-row tiles fill the grid better for the attention-path and
LM-head GEMVs. TPOT 6.97 -> 6.78 ms.
With the reduced barrier count and BLOCK_M=8, 184 programs measures fastest.
TPOT 6.78 -> 6.56 ms.
With BLOCK_M=8, a 1024-wide K block markedly speeds the attention-path and
LM-head BF16 GEMVs. TPOT 6.56 -> 5.87 ms.
The LM head spans the full vocabulary, so a separate BLOCK_M_LM lets it use
a larger row tile than the small attention GEMVs.
bench_islosl.py sweeps ISL:OSL pairs and reports TTFT, TPOT and end-to-end
latency. README now carries a measured table (100:100, 1024:100, 1024:1024,
2048:2048) on MI355X: TPOT is flat at ~5.9 ms/token; TTFT scales linearly
with input length since prefill reuses the single-token decode kernel.
Convert the inner K-reduction of every GEMV (expert FP4 dot_scaled, the
BF16 attention/router GEMVs and the LM head) from a plain while-loop to a
tl.range pipelined loop so the next block's loads overlap the current dot.
This lifts the FP4 expert GEMV from ~1.8 to ~2.7 TB/s. TPOT 5.78 -> 5.20 ms.
Copilot AI review requested due to automatic review settings June 24, 2026 15:56
@neoblizz
neoblizz requested review from BKP and mawad-amd as code owners June 24, 2026 15:56
@github-actions github-actions Bot added in-progress We are working on it iris Iris project issue labels Jun 24, 2026
@neoblizz
neoblizz marked this pull request as draft June 24, 2026 15:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds a GPT-OSS-120B “megakernel” example for Iris, including a single-GPU persistent Triton kernel implementation, reference math, multi-GPU prototype, and tooling for validation/benchmarking/accuracy evaluation.

Changes:

  • Introduces the single-GPU persistent Triton megakernel + reusable common/ device-op library.
  • Adds reference implementation, phased Triton runner, and multiple validation/benchmark scripts.
  • Adds a multi-GPU (1 attention/tail + 4 MoE ranks) prototype using Iris symmetric heap.

Reviewed changes

Copilot reviewed 32 out of 34 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
examples/33_gpt_oss_megakernel/tokenizer_util.py Minimal tokenizer wrapper for HF snapshot tokenizer.json.
examples/33_gpt_oss_megakernel/test_quant_expert.py Validates quantized expert GEMV path vs PyTorch reference.
examples/33_gpt_oss_megakernel/test_kernels.py Validates phased Triton kernels vs reference math.
examples/33_gpt_oss_megakernel/test_dot_scaled.py Validates tl.dot_scaled operand layout for FP4xFP8 GEMV.
examples/33_gpt_oss_megakernel/test_barrier.py Tests grid-wide barrier semantics needed for persistent kernels.
examples/33_gpt_oss_megakernel/run_triton_phased.py End-to-end decode using per-phase Triton kernels orchestrated by host.
examples/33_gpt_oss_megakernel/run_reference.py End-to-end decode using PyTorch reference implementation.
examples/33_gpt_oss_megakernel/reference.py Defines numerical ground-truth for decode forward pass.
examples/33_gpt_oss_megakernel/multi_gpu/run_multi_gpu.py Multi-process multi-GPU driver using Iris symmetric heap exchanges.
examples/33_gpt_oss_megakernel/multi_gpu/protocol.py Defines rank roles and exchange protocol for multi-GPU decode.
examples/33_gpt_oss_megakernel/multi_gpu/persistent_kernels.py Persistent per-rank Triton kernels using device-side flag rendezvous.
examples/33_gpt_oss_megakernel/multi_gpu/moe_kernels.py MoE-rank per-layer expert kernels + scatter-back helper.
examples/33_gpt_oss_megakernel/multi_gpu/attn_kernels.py Attention-rank prologue/scatter/accumulate/lm_head kernels.
examples/33_gpt_oss_megakernel/load_hf.py Loads HF checkpoint into the example’s weight layout + FP4 dequant helpers.
examples/33_gpt_oss_megakernel/kernels.py Standalone Triton building blocks for phased execution.
examples/33_gpt_oss_megakernel/gpt_oss_120b_quantized_megakernel.py Main single-GPU persistent megakernel + host driver.
examples/33_gpt_oss_megakernel/convert_to_iris.py Converts HF checkpoint to .iris tensor archive for mmap/device loading.
examples/33_gpt_oss_megakernel/common/swiglu.py Device ops for SwiGLU (BF16 + FP8-quant variants).
examples/33_gpt_oss_megakernel/common/router.py Device op for top-k + softmax routing.
examples/33_gpt_oss_megakernel/common/rmsnorm.py Device op to materialize residual+RMSNorm output for non-quant path.
examples/33_gpt_oss_megakernel/common/quant.py Device op for fused residual+RMSNorm+FP8 activation quantization.
examples/33_gpt_oss_megakernel/common/gemv_fp8.py FP8 weight-only GEMV device helpers (incl. RMSNorm/resid fused).
examples/33_gpt_oss_megakernel/common/gemv_fp4.py MXFP4 expert GEMV device helpers (dequant path + dot_scaled path).
examples/33_gpt_oss_megakernel/common/gemv_bf16.py BF16 GEMV device helpers (tiled + fused RMSNorm/resid RMSNorm).
examples/33_gpt_oss_megakernel/common/fp4.py FP4 magnitude LUT helper for dequant path.
examples/33_gpt_oss_megakernel/common/barrier.py Grid-wide barrier helper for persistent kernels.
examples/33_gpt_oss_megakernel/common/attention.py Device ops for RoPE+KV append and per-head flash decode with sinks.
examples/33_gpt_oss_megakernel/common/init.py Re-exports reusable device ops for megakernel and multi-GPU kernels.
examples/33_gpt_oss_megakernel/bench_tpot.py TPOT benchmarking script for steady-state decode.
examples/33_gpt_oss_megakernel/bench_islosl.py ISL/OSL sweep benchmark for TTFT/TPOT/E2E throughput.
examples/33_gpt_oss_megakernel/acc_eval.py Accuracy comparison between BF16-attn and FP8-attn variants.
examples/33_gpt_oss_megakernel/README.md Documentation: architecture, usage, benchmarking, and accuracy notes.
.gitignore Adds tmp/ ignore entry.
Comments suppressed due to low confidence (1)

examples/33_gpt_oss_megakernel/tokenizer_util.py:1

  • Snapshot selection always returns the lexicographically-last snapshot directory, without checking that it actually contains tokenizer.json. If the newest snapshot is partial/corrupt, tokenizer loading will fail later with a less actionable error. Consider filtering candidates to those containing tokenizer.json, and/or raising an error that includes attempted paths when none match.

Comment on lines +56 to +57
NUM_WG = 180
_NWG = tl.constexpr(NUM_WG)
Comment on lines +148 to +157
for t in range(lo, pos + 1):
kt = tl.load(kcache_ptr + t * kv_dim + kvh * DH + d).to(tl.float32)
score = tl.sum(q * kt, axis=0) * scale
m_new = tl.maximum(m, score)
alpha = tl.exp(m - m_new)
p = tl.exp(score - m_new)
l = l * alpha + p
vt = tl.load(vcache_ptr + t * kv_dim + kvh * DH + d).to(tl.float32)
acc = acc * alpha + p * vt
m = m_new
Comment on lines +15 to +22
tl.inline_asm_elementwise(
"buffer_inv sc0\n\ts_waitcnt vmcnt(0)",
"=r",
[],
dtype=tl.int32,
is_pure=False,
pack=1,
)
Comment on lines +121 to +122
idx_path = os.path.join(snap, "model.safetensors.index.json")
weight_map = json.load(open(idx_path))["weight_map"]
Comment on lines +1 to +3
"""Accuracy comparison: FP8-attention vs BF16-attention megakernel.

Both share the FP4 experts; this isolates the error introduced by quantizing the
mawad-amd and others added 21 commits August 8, 2026 03:15
…date

The grid barrier ends with `buffer_inv sc0`, which corrupts decode output.
`buffer_inv` discards dirty lines rather than writing them back, and on MI355X
the L2 is per-XCD and shared with ~31 other workgroups. By the time one program
leaves the spin, peers have already passed the barrier and started writing the
next phase, and a cache-wide invalidate throws those writes away.

Measured on GPT-OSS-120B decode, 300 fresh-model reps per arm, one variable,
same node/script/prompt, each arm verified from the binary it ran:

    invalidate            corrupt runs      distinct wrong outputs
    (none)                     0/300                 0
    buffer_inv sc0            15/300                 1
    buffer_inv sc1           293/300                21
    buffer_inv sc0 sc1       300/300                 6

Monotonic in invalidation strength, so a stronger fence makes it worse, not
better. `sc0 sc1`'s dominant failure is byte-identical to the `sc0` failure, so
scope sets the frequency of one failure mode rather than its identity. The
no-invalidate arm is Fisher one-sided p = 2.6e-05 against shipped and is
perf-neutral (4.65 -> 4.59 ms bf16; 4.40 -> 4.41 with --fp8-attn, both inside
run-to-run noise).

Add `_barrier_noinv` and use it from the single-GPU megakernel. `_barrier` is
left as-is: `multi_gpu/` still calls it, that path exchanges over the symmetric
heap rather than an intra-GPU L2, and it has not been tested against this
change. Correctness of the no-invalidate form relies on there being no
cross-phase reuse of shared addresses, which holds for BS=1 decode where
weights stream read-once; an all-to-all pattern fails 0/10 without an
invalidate. Both caveats are documented at the definition.

Co-Authored-By: Claude <noreply@anthropic.com>
Polling with `atomic_add(ptr, 0)` looks like an obvious thing to optimise: it is
an unbounded RMW on a single cache line from every workgroup, and swapping it
for a cache-bypassing load makes the barrier ~1.9x cheaper and the whole token
~1.2x faster.

It also corrupts the output. Measured on GPT-OSS-120B decode with no other
change: 25/25 runs non-reference with 25 distinct trajectories, against 0/300
with the RMW. The counting semantics are not the problem -- the counter is
monotonic and the test is `>=`, so a stale poll can only read too small and
spin longer, never exit early. The RMW is doing a second job: forcing a
coherence point that the payload reads depend on. That is what makes a barrier
with no cache invalidate sound in the first place, and a load that bypasses
cache for the counter address only removes it for everything else.

The scattered failure (25 distinct trajectories) is a genuine race, distinct
from the single-trajectory corruption caused by `buffer_inv`.

Co-Authored-By: Claude <noreply@anthropic.com>
The previous comment asserted that the atomic "is forcing a coherence point
that the payload depends on". That is one of two candidate mechanisms and
neither is confirmed: the alternative is that the RMW contention simply delays
every workgroup long enough for peers' writes to drain, in which case the
barrier is correct by being slow. Adding a single relaxed atomic back at the
spin exit does not restore correctness, so it is not one discrete coherence
event.

A false explanation is worse than an admitted gap -- it tells the next reader
they understand the constraint well enough to engineer around it. State the
measurement, state that the mechanism is open, and give the gate: any change
that speeds up this loop needs a >=300-rep output-correctness run.

Also records why the obvious check does not work. An assert that the counter
reached NB*NWG proves every arrival eventually happened; it says nothing about
whether any program waited at each barrier, so workgroups can run ahead and the
total still lands exactly on target.

Co-Authored-By: Claude <noreply@anthropic.com>
The previous wording said correctness "relies on there being no cross-phase
reuse of shared addresses -- true for BS=1 decode". The condition was right and
the framing was misleading: this kernel has no batch dimension and cannot run
anything but single-sequence decode, so "safe at BS=1" describes a regime it can
never leave and points a reader at a test that does not apply.

The audience is someone reusing this barrier in a different kernel. What they
need to check is whether their programs re-read, across a barrier, shared
addresses they already have cached -- not what batch size they run. State the
measurement on both sides of that condition: 0/300 correct where reuse is
absent, 0/10 with a 64-program all-to-all that re-reads the same slots every
phase, which passes with an invalidating barrier.

Co-Authored-By: Claude <noreply@anthropic.com>
The docstring reported 0/300 without saying where it came from. The run was on
a node that was near-idle across the reps where failures appear, so the figure
describes a quiet machine and says nothing about a busy one.

That matters here because the failure being fixed is timing-sensitive.
Neighbours on other GPUs cannot reach this GPU's L2, but they can lower clocks
through the board power cap, and every timing in the barrier moves with them.
This barrier's correctness has never been observed past the early quiet stretch
under known load.

No claim of a defect is intended: the only contrary observation is a single
non-reference run out of 100 on a shared node, which is not distinguishable from
the 0/300 at this n. Stating the conditions is true regardless of how that
resolves, and a measurement without its conditions invites the reader to assume
the widest one.

Co-Authored-By: Claude <noreply@anthropic.com>
The previous note said behaviour under heavy node contention was
uncharacterised, and gave the board power cap as the mechanism to worry about.
That mechanism has since been measured and is absent: GPU 0 under this workload
runs at 2388 MHz on an idle node and 2392 MHz with seven sibling GPUs saturated.
Neighbours cannot reach this GPU's L2 and do not move its clock, so there is no
established channel by which a co-tenant perturbs the race this barrier is
exposed to.

Removing that explanation makes the one contrary observation more likely to be
real, not less, so the caveat now states the thing itself: pooled across every
run of this barrier the rate is 1/475 (0.21%, 95% CI 0.01-1.17%), and a residual
defect is not excluded.

It remains the right choice by a wide margin. The barrier it replaces measured
15/300 (5.0%) on a quiet node -- roughly 24x higher, Fisher one-sided p = 5e-06.
The honest framing is a large improvement with an uncharacterised remainder
rather than a clean barrier.

Co-Authored-By: Claude <noreply@anthropic.com>
Two problems with the figure as committed. It included a count from a run that
was still executing, so the denominator was stale before the commit landed. And
it presented the pooled rate ahead of the reasoning that makes pooling
legitimate, so a reader who stops at the table takes a cross-node average at
face value.

Pooling across nodes assumes the rate does not depend on the node, which is the
exact question the single event raised. That assumption is only defensible
because the mechanism was measured and found absent -- neighbours do not move
this GPU's clock and cannot reach its L2. If a channel is ever established, the
figure has to be split by venue rather than pooled, and the note now says so.

Co-Authored-By: Claude <noreply@anthropic.com>
The note listed two candidate mechanisms for why the RMW poll is load-bearing:
that it orders other memory operations across the spin exit, or that its cost
simply delays every workgroup long enough for peers' writes to drain. The second
is now eliminated by measurement.

Substituting a pure delay of 27x the RMW's measured per-barrier cost (s_sleep
4096, ~109 us against ~4.00 us) is 25/25 corrupt with 25 distinct trajectories.
An earlier delay arm was dismissible on two grounds -- it ran at 43% of the dose
and it shared a node -- and this one is neither: the dose is 27x and it ran
exclusive, so a dirty result is interpretable.

This narrows the caveat rather than widening it. The barrier's correctness is
not an accident of how expensive the poll happens to be, so nothing here is
contingent on timing. What remains unexplained is narrower: something about the
repeated RMW's memory-model semantics, given that one atomic at the exit and a
waitcnt drain both fail to substitute.

Co-Authored-By: Claude <noreply@anthropic.com>
The note claimed this barrier is ~24x better than the one it replaces. Both
numbers are real and the ratio between them is not: the numerator pools a quiet
node with a shared one, the denominator is a quiet node only, and the two
barriers have never been measured side by side at the same n on the same
machine. That is the same defect as dividing a numerator and denominator from
different runs, one level up -- different venues rather than different jobs.

A paired run now in progress has the old barrier at 0/175 under synthetic load
where it historically failed at roughly 10% past that point. That is 25
informative reps and not decisive, but it is the wrong direction to be leaning
on an unpaired ratio while the paired measurement is still running.

The decision this note supports does not rest on the ratio. The sc1 variants
measured 293/300 and 300/300; no venue argument reaches numbers of that size, so
removing the invalidate stands on its own regardless of where the low-rate
figures settle.

Co-Authored-By: Claude <noreply@anthropic.com>
The in-flight count this note flagged as provisional has completed: a full 0/300
on an exclusive node, which is the first clean run this barrier has had under
conditions that were characterised rather than lucky. That takes the pooled
figure to 1/700.

Reporting it pooled hides the structure. Every run on a machine whose conditions
we understand is clean -- 0/600 combined -- and the single non-reference output
came from an organically busy shared node, which is the one venue we could not
characterise. The note now splits by machine and gives an interval per venue.

This is deliberately not phrased as reassurance. 100 reps on the busy node is
thin, and the synthetic load we built to reproduce that condition was measured
inert: it saturates sibling GPUs, and sibling GPUs do not move this GPU's clock
(2388 MHz idle-node vs 2392 MHz stressed). A condition we cannot reproduce on
demand leaves the rate there unknown rather than merely unmeasured.

Co-Authored-By: Claude <noreply@anthropic.com>
This note described the barrier being replaced as measuring 15/300 (5.0%) "on a
quiet node". Both halves of that are wrong.

The node was not quiet: a co-tenant occupied it for the first ~117 of the 300
reps, and every one of the 15 failures landed after that neighbour left. And 5%
is not the barrier's rate -- the identical file under a controlled synthetic
load on another machine is 0/300, against 15/300 on the first, Fisher one-sided
p = 2.6e-05 at full n on both sides.

Both numbers are real measurements of the same code, so the rate depends on the
machine through a channel we have not identified. It is not the clock (GPU 0
measures 2388 MHz idle-node vs 2392 MHz stressed) and it is not the L2, which
neighbours cannot reach; what remains is host-side, such as launch cadence or
per-rep build timing.

The decision to remove the invalidate does not rest on either figure. The sc1
variants measured 293/300 and 300/300 and fire in every venue tested.

Co-Authored-By: Claude <noreply@anthropic.com>
… need it

The rate this change was justified by came from a single session. The identical
barrier file has since run 0/300 under a controlled synthetic load and 0/300 on
a node verified as sole occupant before the run started -- 600 further reps in
two venues with no failures, Fisher one-sided p = 2.6e-05 against the original.
There is no reproducible failure rate at the shipping dose and none should be
quoted.

The failures themselves were real: 15 byte-identical prompt-echo trajectories is
not noise. They remain unexplained. The only pattern consistent with every run
is that they began after a co-tenant left the machine mid-run, which is a
transition rather than a state, and none of these measurements isolate it.

Removing the invalidate remains correct on an argument that never depended on
that number. Removal has zero measured cost, keeping the operation has no
demonstrated benefit, the operation is destructive by mechanism and catastrophic
at stronger scopes (293/300 and 300/300, reproducing in every venue at full n),
and nothing has failed in any venue after removing it. No single clause carries
that; together they hold without a reproducible rate.

Co-Authored-By: Claude <noreply@anthropic.com>
…e one family

The note said the 15/300 failed to reproduce across "two venues". Both of those
venues are the same node family, and both failing runs are the same machine, so
it is one family sampled twice against one machine sampled twice rather than
four independent points.

Sorting every run of the identical file by host rather than by load also removes
the load story entirely: the machine that fails does so both with a co-tenant
and under a synthetic load, and the machines that pass do so both under load and
solo. A later run showed a failure at rep 50 with the load still applied, which
also rules out the busy-then-idle transition that the earlier ordering suggested.

Host is a label for whatever differs, not a mechanism. Slurm reports these nodes
as identical in version, GRES, CPU count and memory, so the difference is below
what the scheduler exposes. A run on another node of the failing family would
separate "this machine" from "this family" and has not been done.

Co-Authored-By: Claude <noreply@anthropic.com>
The failures of the barrier being replaced sort by host: they occur on one
machine and have never occurred on the two others tested. Averaging a rate over
hosts that do not exhibit the defect understates it on the host that does, and
that average is what this note has been quoting.

Restricting to that machine gives a comparison with no cross-venue or cross-run
division on either side: the old barrier is 19/575 across two runs under two
different load conditions, and the replacement is 0/600 across two runs paired
in a single container. Fisher one-sided p = 1.1e-06.

This is a better justification for the change than the rate it originally
shipped with, because it is measured where the defect actually appears. The
replacement's runs predate knowing which machine was interesting, so they were
not designed as this comparison; they happen to constitute one.

Co-Authored-By: Claude <noreply@anthropic.com>
The old barrier's second run on that host has finished at 4/300, so its total
there is 19/600 rather than the 19/575 partial this note quoted. The replacement
is 0/600 on the same host, giving equal denominators on both sides and Fisher
one-sided p = 1.7e-06.

The previous figure counted a run that was still executing, which is the same
defect this file already corrected once for a different number.

Co-Authored-By: Claude <noreply@anthropic.com>
The note said a run on another node of the failing family would separate "this
machine" from "this family" and had not been done. It has now: thor-3, same
family as the failing thor-2, is 0/300 at full length on a node verified as sole
occupant before the run.

So the old barrier's failures are specific to one machine. Load is fully
eliminated as the variable -- that machine fails both with a co-tenant and under
synthetic load, and every passing machine passes both loaded and solo.

Stated with its power rather than as a clean result: 0/300 on thor-3 excludes
thor-2's measured rate (P = 0.008 at 1.6%) but not the bottom of its confidence
interval (P = 0.27 at 0.44%). And thor-2's own two runs differ by 4x, 15/300
against 4/300, so the host sorts whether it fails without producing a stable
rate underneath.

Co-Authored-By: Claude <noreply@anthropic.com>
The note gave one power figure, computed against a confidence floor from a
single run of the failing host. Pooling that host's two runs instead moves the
floor from 0.36% to 1.92% and takes the same clean 300 from "not excluded"
(P = 0.33) to "decisive" (P = 0.003), with the two hosts' intervals disjoint.

Neither number is wrong; they answer different questions. Pooling assumes the
two runs on that host sample one rate, and they differ 4x -- 15/300 against
4/300 -- so the conservative statement is the unpooled one. Both are now given,
along with the point that the gap between them is a choice about pooling rather
than anything measured on the clean host.

Co-Authored-By: Claude <noreply@anthropic.com>
Four small defects found while studying this example, none related to the
barrier work.

`--persistent` multi-GPU raises `KeyError: MEASURE_NOEXCH` before it reaches a
kernel: run_multi_gpu.py passes the flag at line 227 and it is absent from
attn_persistent_kernel's signature, so that path cannot run at all. Add the
constexpr and let it gate the result rendezvous, which is what the flag is for.
The output with it set is garbage by construction -- res[] holds the previous
layer's values -- so the docstring says it is a profiling switch and not a
correctness path.

Add a run-time check that NUM_WG fits the device. The grid-wide barrier needs
every program resident and this kernel gets one workgroup per CU, so a grid
above the CU count hangs with no error rather than failing. The boundary tracks
the CU count rather than a constant -- 80/81 on MI308X, 256/257 on MI355X,
304/305 on MI300X -- so it has to be checked against the device, not asserted.

Add requirements.txt. The base images ship torch and triton but not tokenizers,
safetensors or huggingface_hub, and the example does not run without them.

Remove `bars_per_layer = 11`. Nothing reads it and it matches neither path: the
quant path executes 7 barriers per layer and the bf16 path 8. A stale constant
that looks like an invariant invites someone to compute a barrier target from
it.

Co-Authored-By: Claude <noreply@anthropic.com>
The note told a reader whose kernel re-reads shared addresses across a barrier
to use `_barrier` instead. That does not work, and the measurement contradicting
it was already in hand when the advice was written.

The 64-program all-to-all test was run against all four barrier variants. The
L1-only invalidate that `_barrier` uses fails it 0/10, exactly as no invalidate
does; only an invalidate reaching L2 passes.

    no invalidate       0/10  FAIL
    buffer_inv sc0      0/10  FAIL   <- what _barrier does
    buffer_inv sc1     10/10  pass
    buffer_inv sc0 sc1 10/10  pass

So neither barrier in this file is correct for that access pattern, and pointing
at `_barrier` sends someone to a variant that fails the same way. The note now
gives the table and says so.

Co-Authored-By: Claude <noreply@anthropic.com>
The test used one monotonic counter for both edges of each phase. That is
lappable -- a fast program's own later arrival can satisfy a target meant to
require all P distinct programs -- so a program could overwrite its slot while a
slower one was still summing. It failed roughly 3 runs in 10, and because the
failure is a race rather than a wrong answer, the single run it did made that
invisible.

Add the second counter so reads complete before the next phase's writes, and run
the whole thing ten times, since one green run of a race test is not evidence.

Also record why the barrier is inlined instead of imported, which previously
looked like an oversight. Every program here re-reads all N slots every phase,
which is the cross-phase reuse the shipped barriers document as out of scope,
and both fail it:

    no invalidate      0/10  FAIL
    buffer_inv sc0     0/10  FAIL
    buffer_inv sc1    10/10  pass
    buffer_inv sc0 sc1 10/10 pass

The inlined spin uses sem="acquire", which lowers to buffer_inv sc1 on gfx950.
Importing either shipped barrier would fail, and that is a property of this
test's all-to-all sharing rather than a defect in them.

Co-Authored-By: Claude <noreply@anthropic.com>
These kernels use a grid-wide barrier at 20 call sites across three files and
never check that NWG fits the device. Above the CU count some programs are never
scheduled and the rest spin on a target that cannot be reached, so the failure
is a hang with no error. NWG=120 fits the parts in use and not an 80-CU MI308X.

The single-GPU path gained a run-time check for this; this path did not, because
it has no test coverage here and adding the import blind is a worse trade than
writing down the hazard. Moving the check into common/ or duplicating it locally
are both reasonable, and either should be done by someone able to run the path.

Recording one interaction explicitly: the --persistent entry point holds 13 of
the 20 sites and used to raise KeyError before launching anything, so the hazard
could not fire there. Fixing that KeyError made those sites reachable. That is
still an improvement over a path that cannot run at all, but the note describes
the code as it is now rather than as it was.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in-progress We are working on it iris Iris project issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants