Skip to content

refactor(engine/hip): kernels take the QuixiCore shape — a family per folder - #22

Merged
Snider merged 1 commit into
mainfrom
lane/hip-kernel-layout
Aug 6, 2026
Merged

refactor(engine/hip): kernels take the QuixiCore shape — a family per folder#22
Snider merged 1 commit into
mainfrom
lane/hip-kernel-layout

Conversation

@Snider

@Snider Snider commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The 17,090-line rocm_kernels.hip becomes a 72-line amalgamation TU over 59 contiguous fragments in kernels/<family>/ (contract · common · quant · attention · serving · sampling · norms · moe · embedding · matmul · activations · training · lora), following the QuixiCore taxonomy adapted to this engine (the variants/<target> axis is deliberately unused — one source compiles AMD/NVIDIA/CPU; the README records where it goes if arch-specific kernels arrive).

Byte-identity receipts: assembled source sha256 identical to HEAD's mega file (454c04d0…); all 142 exported kernel bodies identical; the only diff is two manifest banner comments. A manifest-coverage test fails on any fragment on disk but unwired, wired twice, or missing its licence header (negative control run included).

Validation: Mac GOWORK=off go build ./... + vet clean; homelab compiles at identical output sizes on ROCm gfx1101, CUDA 12.8, and HIP-CPU x86_64; go test ./engine/hip ok; the 42 TestHIPKernelSource_ tests pass over the new layout. task test's engine/metal failure is pre-existing at the base commit (symlinked config.json rejected by the sandboxed reader) and untouched by this diff.

🤖 Generated with Claude Code
Co-Authored-By: Virgil virgil@lethean.io

Summary by CodeRabbit

  • New Features

    • Added broad AMD GPU acceleration for attention, projections, quantisation, embeddings, sampling, mixture-of-experts, normalisation, serving, diffusion and training workloads.
    • Added support for chunked attention, rotary encoding, KV-cache management, multiple quantised formats, greedy and top-k sampling, and embedding operations.
    • Added specialised paths for different hardware targets and workload shapes.
  • Documentation

    • Documented kernel structure, ordering, supported targets and guidance for extending GPU functionality.
  • Tests

    • Improved coverage to verify kernel assembly, manifest completeness, licensing headers and existing runtime behaviour.

… family

rocm_kernels.hip was one 17,090-line mega file. It is now a 72-line
amalgamation translation unit: preamble, the anonymous namespace, and an
ordered #include manifest over 59 fragments in 13 semantic families —
contract, common, activations, attention, embedding, lora, matmul, moe,
norms, quant, sampling, serving, training. The taxonomy is QuixiCore's
(kernels/<family>/, contract central, implementations family-local),
adapted: no variants/<target>/ tree, because this engine compiles ONE
source for AMD, NVIDIA and HIP-CPU alike, so the variant axis is empty.

Pure reorganisation — no maths changed, no kernel symbol renamed. Every
fragment is a contiguous slice of the original TU, so the manifest
reassembles it exactly.

Byte-identity receipts:
  - all 142 exported kernel bodies hash-identical before -> after
    (sha256 of the 142-row table: 38680f7805edeffbb5028bb8fda9b0c1d0d08b98cb037864cd5f91a71709f3e4)
  - reassembled source == HEAD's rocm_kernels.hip (454c04d0...) except
    the manifest's own two `// ---` phase banners; nothing else differs
  - the anonymous namespace (lines 8..5092 of the original, four KV
    kernels inside it) is preserved verbatim — ordering is load-bearing
    in a single TU and the manifest phases encode it

The manifest is explicit, never a glob: a fragment on disk that nobody
wired would compile to nothing. TestHIPKernelSource_FragmentManifestCovers
EveryFile_Good fails on an unwired or twice-wired fragment (verified
against a planted orphan) and on a missing licence header. The 34 source
assertions read through hipReadKernelSource, which resolves the manifest,
so every existing check runs against the same assembled text.

Receipts — Mac (darwin/arm64):
  GOWORK=off go build ./...        exit 0
  GOWORK=off go vet ./...          exit 0
  task test                        FAIL, pre-existing: engine/metal
    TestLaneSetThroughputAB + TestLaneSetGEMME2BByteIdentityHiddens fail
    on a symlinked gemma4e2b/config.json ("path escapes from parent").
    Same FAIL at 2727995 before this change; this lane touches no file
    under engine/metal.

Receipts — homelab (linux/amd64, ROCm 7.2, gfx1101, CUDA 12.8):
  make hip-amd AMD_HIP_ARCH=gfx1101   exit 0   1386800 B (= baseline)
  make hip-nvidia CUDA_PATH=cuda-12.8 exit 0   5992208 B (= baseline)
  make hip-cpu-x86_64                 exit 0  33576000 B (= baseline)
  go build ./...                      exit 0
  go vet ./engine/hip                 exit 0
  go test ./engine/hip -count=1       ok 4.587s
  42 TestHIPKernelSource_* pass over the new layout

Object hashes are NOT compared: hipcc is not byte-deterministic here —
two compiles of one unchanged source differ in 904 bytes. Output sizes
match on all three lanes; the identity claim rests on the source.

Co-Authored-By: Virgil <virgil@lethean.io>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a manifest-based HIP kernel source assembly path and documentation. It introduces a shared launch ABI, device validators, and ROCm kernels for attention, quantisation, serving, sampling, embeddings, normalisation, routing, activations, projections, and training losses. Existing source tests now inspect assembled fragments.

Changes

ROCm HIP kernel platform

Layer / File(s) Summary
Fragment assembly and shared contracts
go/engine/hip/hip_kernel_source_test.go, go/engine/hip/kernels/README.md, go/engine/hip/kernels/contract/*, go/engine/hip/kernels/common/*
Tests resolve manifest-listed .hipinc fragments and verify coverage and licence headers. The ABI and common helpers define shared launch structures, numeric conversions, reductions, and platform-specific intrinsics.
Attention and KV access
go/engine/hip/kernels/attention/*
Adds validated single-head, multi-head, causal, lane-batched, fused RMS/RoPE, rotary, and chunked attention paths. Adds descriptor-based and contiguous quantised KV access.
Quantised projection and sampling kernels
go/engine/hip/kernels/quant/*, go/engine/hip/kernels/sampling/*
Adds affine, GGUF, Jangtq, AutoRound, GELU-tanh, projection, greedy, top-k, diffusion, and sampling kernels with format-specific validation and decoding.
Normalisation, activation, and basic projections
go/engine/hip/kernels/activations/*, go/engine/hip/kernels/norms/*, go/engine/hip/kernels/lora/*, go/engine/hip/kernels/matmul/*
Adds elementwise operations, RMS normalisation variants, fused RMS projection, LoRA projection, and FP32/BF16/Q8/FP16 matrix projections.
Embeddings and expert routing
go/engine/hip/kernels/embedding/*, go/engine/hip/kernels/moe/*
Adds embedding lookup, mean pooling, cosine reranking, expert routing, routed projections, route reduction, and lazy expert residency kernels.
KV serving paths
go/engine/hip/kernels/serving/*
Adds KV encoding, value normalisation, descriptor append and trimming, prefill/decode status kernels, and tiny-model prefill/decode execution.
Training loss kernels
go/engine/hip/kernels/training/*
Adds cross-entropy, distillation KL, and GRPO advantage kernels with launch validation.

Sequence Diagram(s)

sequenceDiagram
  participant LaunchPacket
  participant DeviceValidator
  participant HIPKernel
  participant DeviceBuffers

  LaunchPacket->>DeviceValidator: provide ABI metadata and buffer descriptors
  DeviceValidator->>DeviceValidator: validate versions, sizes, pointers, and dimensions
  DeviceValidator-->>HIPKernel: allow valid launch
  HIPKernel->>DeviceBuffers: read encoded inputs and write outputs
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarises the main change: reorganising HIP kernels into semantic family folders.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Snider
Snider merged commit 649a7d8 into main Aug 6, 2026
0 of 4 checks passed
@Snider
Snider deleted the lane/hip-kernel-layout branch August 6, 2026 12:48

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
go/engine/hip/kernels/README.md (1)

101-104: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the C++ standard requirement.

The NVIDIA and HIP-CPU compile paths use C++20. The statement that the HIP source is built as C++23 can cause contributors to add code that breaks supported backends.

State that AMD builds use C++23 and that shared source must remain C++20-compatible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/README.md` around lines 101 - 104, Update the
build-requirement statement in the HIP kernels README to say that AMD builds use
C++23 while shared source must remain compatible with C++20 for NVIDIA and
HIP-CPU compile paths. Remove the claim that HIP source generally uses C++23,
while preserving the existing cgo and mdspan guidance.
🟠 Major comments (24)
go/engine/hip/kernels/training/losses.hipinc-91-94 (1)

91-94: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject non-finite rewards before device dispatch.

hipLoadedModel.RunGRPOAdvantage accepts any non-empty reward slice at go/engine/hip/hip_training_launch.go Lines 732-747. A NaN or infinity reaches this guard. The guard returns without writing output. The launcher then reads the output after a successful kernel launch, so the caller receives no error for an invalid advantage result.

Validate every reward on the host and return an error before hipRunGRPOAdvantageKernel.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/training/losses.hipinc` around lines 91 - 94, Validate
every element of the reward slice in hipLoadedModel.RunGRPOAdvantage before
invoking hipRunGRPOAdvantageKernel, rejecting NaN and infinite values with an
error. Keep the existing non-empty input checks and ensure invalid rewards
return before device dispatch or output access.
go/engine/hip/kernels/contract/launch_abi.hipinc-1848-1850 (1)

1848-1850: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add the missing packed top-k ABI assertion.

rocm_packed_topk_launch_args has no static_assert against ROCM_PACKED_TOPK_LAUNCH_ARGS_BYTES. A field or alignment change can then compile while the launcher packet contract has drifted.

Proposed fix
+static_assert(sizeof(rocm_packed_topk_launch_args) == ROCM_PACKED_TOPK_LAUNCH_ARGS_BYTES, "packed top-k launch ABI drift");
 static_assert(sizeof(rocm_packed_topk_sample_launch_args) == ROCM_PACKED_TOPK_SAMPLE_LAUNCH_ARGS_BYTES, "packed top-k sample launch ABI drift");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/contract/launch_abi.hipinc` around lines 1848 - 1850,
Add a static_assert for rocm_packed_topk_launch_args in the ABI assertion
section, comparing sizeof(rocm_packed_topk_launch_args) with
ROCM_PACKED_TOPK_LAUNCH_ARGS_BYTES and using a clear packed top-k launch ABI
drift message. Leave the existing assertions unchanged.
go/engine/hip/kernels/attention/single_head.hipinc-836-841 (1)

836-841: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Remove the unused per-token page lookup in the fallback loop.

Line 838 assigns page and never reads it. Line 839 calls rocm_attention_device_kv_value, which resolves the page again through rocm_attention_device_kv_page.

This loop is nested inside the dim loop at Line 834. For the device-KV path the code therefore performs args.dim * args.token_count redundant descriptor lookups, and each lookup can run a binary search over page_count. Delete the assignment.

Note: this fragment is a move of pre-existing code, so this is not a regression introduced by the PR.

⚡ Proposed fix
 		for (uint32_t dim = tid; dim < args.dim; dim += threads) {
 			float out = 0.0f;
 			for (uint32_t token = 0; token < args.token_count; ++token) {
 			const uint32_t base = token * kv_stride;
-			const rocm_device_kv_page_descriptor *page = device_kv ? rocm_attention_device_kv_page(args, token) : nullptr;
 			const float value = device_kv ? rocm_attention_device_kv_value(args, false, token, dim) : values[base + dim];
 			out += weights[token] * value;
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/attention/single_head.hipinc` around lines 836 - 841,
Remove the unused page assignment calling rocm_attention_device_kv_page from the
per-token fallback loop around rocm_attention_device_kv_value, while preserving
the base, value, and accumulation logic.
go/engine/hip/kernels/attention/chunked.hipinc-96-110 (1)

96-110: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate page geometry on the direct token-page fast path.

When direct_kq8vq4_token_pages is true, Line 97 reduces validation to three null checks. The slow path at Line 99 instead calls rocm_attention_kq8vq4_page_valid(page, token, kv_width), which checks token_count, key_width == kv_width, value_width == kv_width, the q8 and q4 encodings, and the declared key_bytes and value_bytes.

The fast path then uses the unchecked fields. Line 105 adds kv_dim_offset to the key payload pointer, Line 109 adds (value_base + kv_dim_offset) >> 1u to the value payload pointer, and Lines 117-120 read key_values[dim] for every dim < args.dim. If a page reports key_width smaller than kv_width = args.key_heads * args.dim, those reads run past the page allocation. The header mode_code == ROCM_DEVICE_KV_DESCRIPTOR_MODE_KQ8VQ4 check at Line 71 describes the stream, not each page.

The same asymmetry repeats at Lines 147-151, 441-445, 492-496, 730-734, 793-797, 1103-1107, 1165-1167, 1447-1451 and 1525-1527.

Either check the widths and encodings on the fast path, or hoist a one-time geometry check on the first page before direct_kq8vq4_token_pages is set.

Note: this fragment is a move of pre-existing code, so this is not a regression introduced by the PR.

🛡️ Proposed fix for the fast-path check
 			if (direct_kq8vq4_token_pages) {
-				page_valid = page != nullptr && page->key_pointer != 0 && page->value_pointer != 0 ? 1u : 0u;
+				page_valid = page != nullptr &&
+					page->key_pointer != 0 &&
+					page->value_pointer != 0 &&
+					page->key_width == kv_width &&
+					page->value_width == kv_width &&
+					rocm_device_kv_encoding_is_q8(page->key_encoding) &&
+					rocm_device_kv_encoding_is_q4(page->value_encoding) ? 1u : 0u;
 			} else {
 				page_valid = rocm_attention_kq8vq4_page_valid(page, token, kv_width) ? 1u : 0u;
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/attention/chunked.hipinc` around lines 96 - 110,
Validate full page geometry before using the direct token-page fast path: update
the direct_kq8vq4_token_pages handling around rocm_attention_kq8vq4_page_valid
so it checks token_count, key_width/value_width against kv_width, required q8/q4
encodings, and declared key_bytes/value_bytes, or perform an equivalent one-time
validation before enabling the fast path. Apply the same protection to every
repeated direct-page validation site in this file, including the blocks near the
referenced attention paths, while preserving the existing slow-path validation.
go/engine/hip/kernels/attention/device_kv.hipinc-250-251 (1)

250-251: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the packed int32_t load against a misaligned payload pointer.

row_base_index only checks the row offset within payload. For row-interleaved q8, payload can be misaligned when values is 4-byte aligned but width makes local_token * stride + sizeof(uint32_t) non-4-byte aligned. In that fast path, cast through uintptr_t before the grouped q8 load or skip it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/attention/device_kv.hipinc` around lines 250 - 251,
Update the packed q8 fast path around the row_base_index check to also validate
that values + row_base_index is 4-byte aligned before casting to const int32_t*.
Use a uintptr_t-based alignment check, and fall back to the existing safe load
path when misaligned.
go/engine/hip/kernels/attention/chunked.hipinc-1042-1050 (1)

1042-1050: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Restrict chunk_size to the per-variant shared-memory budget.

rocm_attention_heads_batch_chunked_stage1_gqa4 and gqa8 allocate per-block shared buffers proportional to group_size * chunk_size * dim, but the common config validation only rejects tiny/invalid chunk_size; it does not check the GQA shared-memory size before the selected kernel launch. Add the shared-memory calculation to the selected GQA path or reject configurations whose calculated SharedMemBytes exceeds the chosen kernel’s limit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/attention/chunked.hipinc` around lines 1042 - 1050, Add
per-variant shared-memory validation for the selected GQA path before launching
rocm_attention_heads_batch_chunked_stage1_gqa4 or
rocm_attention_heads_batch_chunked_stage1_gqa8. Calculate SharedMemBytes using
the selected group_size, chunk_size, and dim, then reject configurations
exceeding that kernel’s shared-memory limit while preserving existing validation
for valid configurations.
go/engine/hip/kernels/lora/projection.hipinc-25-41 (1)

25-41: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Hoist the LoRA down-projection out of the per-row loop.

The inner loop at lines 35-37 computes down = sum_col lora_a[r][col] * input[col]. This value depends only on r and input. It does not depend on row. Every thread in the grid therefore recomputes the same rank * cols reduction.

Total work becomes rows * rank * cols instead of rank * cols. For 4096 rows and rank 16, the LoRA term costs about 4096 times more than required, and it dominates the kernel.

The access pattern is also uncoalesced. Consecutive threads read base[row * args.cols + col] at a stride of args.cols. rocm_projection in go/engine/hip/kernels/matmul/projection.hipinc uses one block per row with rocm_block_reduce_sum, which reads contiguously.

Compute down[r] once per block into shared memory, then reuse it for every row. Alternatively, adopt the block-per-row layout used by rocm_projection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/lora/projection.hipinc` around lines 25 - 41, Hoist the
LoRA down-projection out of the per-row computation in the projection kernel:
compute each rank component of down once per block, store the results in shared
memory, synchronize, and reuse them while accumulating delta for each row.
Update the kernel’s launch/shared-memory handling as needed, preserving the
existing bias, scaling, and output behavior.
go/engine/hip/kernels/norms/rms_norm_validate.hipinc-146-147 (1)

146-147: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

32-bit dimension products wrap before widening, in both validators and index arithmetic. Each site multiplies two uint32_t dimension fields. C++ evaluates the product in 32-bit arithmetic and wraps modulo 2^32. Widening by sizeof(float) or sizeof(uint16_t) happens only afterwards, so it does not prevent the wrap. The result is a byte check that accepts an undersized buffer, or a device index that addresses the wrong memory. Several validators in the same files already use the correct 64-bit form, which makes the fix unambiguous.

  • go/engine/hip/kernels/norms/rms_norm_validate.hipinc#L146-L147: compute head_dim * head_count as uint64_t, bound it by 0xffffffffu, and use it in both byte comparisons. Apply the same change to rocm_valid_rms_norm_rope_heads_args at lines 177-178.
  • go/engine/hip/kernels/lora/projection_validate.hipinc#L22-L24: cast args.rows, args.rank and args.cols to uint64_t in the base_weight_bytes, lora_a_bytes and lora_b_bytes comparisons.
  • go/engine/hip/kernels/matmul/projection.hipinc#L29-L29: cast row to uint64_t in weights[row * args.cols + col], and repeat at lines 34, 40, 45, 86, 98, 111 and 123.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/norms/rms_norm_validate.hipinc` around lines 146 - 147,
Prevent 32-bit dimension-product overflow across the validators and index
arithmetic. In go/engine/hip/kernels/norms/rms_norm_validate.hipinc lines
146-147 and 177-178, compute head_dim * head_count as uint64_t, bound it by
0xffffffffu, and reuse it for both byte checks. In
go/engine/hip/kernels/lora/projection_validate.hipinc lines 22-24, cast rows,
rank, and cols to uint64_t in the base_weight_bytes, lora_a_bytes, and
lora_b_bytes comparisons. In go/engine/hip/kernels/matmul/projection.hipinc
lines 29, 34, 40, 45, 86, 98, 111, and 123, cast row to uint64_t before
multiplying by args.cols in each weights index.
go/engine/hip/kernels/norms/rms_norm_fused_projection.hipinc-119-119 (1)

119-119: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Align the output_scale_bits == 0 convention with the other RMS kernels.

Line 119 converts args.output_scale_bits directly. A value of 0 therefore produces a scale of 0.0f and zeroes both residual_output and the projection input.

The sibling kernels treat 0 as "unset" and substitute 1.0f. See go/engine/hip/kernels/norms/rms_norm.hipinc line 97 and line 200, and the matching validators at go/engine/hip/kernels/norms/rms_norm_validate.hipinc line 79 and line 110. The validator for this kernel only checks isfinite(output_scale), so 0 passes validation silently.

If the host always sets a non-zero scale for this kernel, reject 0 in the validator instead. If the host may omit the scale, apply the same substitution here.

🔧 Proposed fix: use the shared convention
-	const float output_scale = rocm_float_from_bits(args.output_scale_bits);
+	const float output_scale = args.output_scale_bits == 0 ? 1.0f : rocm_float_from_bits(args.output_scale_bits);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/norms/rms_norm_fused_projection.hipinc` at line 119,
Update the output-scale handling in the fused projection kernel around
output_scale and its validator so output_scale_bits == 0 follows the shared
RMS-kernel convention and resolves to 1.0f before scaling residual_output or the
projection input. Reuse the existing bit-decoding path and preserve nonzero
scale behavior; only reject zero in the validator if this kernel’s host contract
guarantees the scale is always provided.
go/engine/hip/kernels/matmul/projection_validate.hipinc-5-16 (1)

5-16: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validate the bias pointer and bias size in rocm_valid_projection_args.

rocm_valid_projection_args never inspects args.bias_pointer, args.bias_bytes or ROCM_PROJECTION_LAUNCH_FLAG_BIAS. rocm_projection reads bias[row] at go/engine/hip/kernels/matmul/projection.hipinc line 52 for any row below args.rows. A bias buffer shorter than rows * sizeof(float) produces an out-of-bounds device read.

rocm_valid_projection_batch_args in this file already performs both checks at lines 46-53. Apply the same checks to the scalar validator.

🛡️ Proposed fix
 		args.output_bytes != args.rows * sizeof(float)) {
 		return false;
 	}
+	if ((args.flags & ROCM_PROJECTION_LAUNCH_FLAG_BIAS) != 0 &&
+		(args.bias_pointer == 0 || args.bias_bytes != args.rows * sizeof(float))) {
+		return false;
+	}
+	if ((args.flags & ROCM_PROJECTION_LAUNCH_FLAG_BIAS) == 0 &&
+		(args.bias_pointer != 0 || args.bias_bytes != 0)) {
+		return false;
+	}
 	if (args.weight_encoding == ROCM_PROJECTION_WEIGHT_ENCODING_FP16 ||
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/matmul/projection_validate.hipinc` around lines 5 - 16,
Update rocm_valid_projection_args to validate the bias configuration like
rocm_valid_projection_batch_args: require the bias pointer when
ROCM_PROJECTION_LAUNCH_FLAG_BIAS is set, and require bias_bytes to equal rows *
sizeof(float) when bias is enabled. Preserve validation for configurations
without bias.
go/engine/hip/kernels/embedding/mean_pool_validate.hipinc-11-12 (1)

11-12: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Byte-count checks in the embedding validators use 32-bit products. Both validators multiply two uint32_t count fields together before the sizeof(float) operand promotes the expression to size_t. The 32-bit product can wrap, so a malformed packet can pass a byte-count check that does not cover the buffer, and the kernel then reads out of bounds. The MoE validators in go/engine/hip/kernels/moe/routing_validate.hipinc already apply a leading static_cast<uint64_t> at Lines 41, 42, 43, 55, and 56. Apply that same pattern here.

  • go/engine/hip/kernels/embedding/mean_pool_validate.hipinc#L11-L12: cast args.token_count to uint64_t before multiplying by args.dim, and cast args.dim to uint64_t in the output check.
  • go/engine/hip/kernels/embedding/rerank_validate.hipinc#L13-L13: cast args.document_count to uint64_t before multiplying by args.dim.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/embedding/mean_pool_validate.hipinc` around lines 11 -
12, Prevent 32-bit overflow in the embedding byte-count validators by promoting
operands before multiplication. In
go/engine/hip/kernels/embedding/mean_pool_validate.hipinc lines 11-12, cast
args.token_count before multiplying by args.dim and cast args.dim in the
output-size check; in go/engine/hip/kernels/embedding/rerank_validate.hipinc
line 13, cast args.document_count before multiplying by args.dim. Use the
existing uint64_t promotion pattern from the MoE validators.
go/engine/hip/kernels/moe/routing_validate.hipinc-80-82 (1)

80-82: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Add size checks for the chunk-held MLX affine buffers.

rocm_moe_mlx_affine_routes_chunk only stores pointers for gate_up_* and down_* buffers, and validation rejects zero pointers but reads past args.rows * packed_per_row and args.cols / args.group_size. Model the kernel-expected extents for each supported bit width and validate them at launch to avoid trusting undersized chunks that pass pointer checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/moe/routing_validate.hipinc` around lines 80 - 82,
Update the validation logic in rocm_moe_mlx_affine_routes validation to verify
the byte sizes of every chunk-held gate_up_* and down_* buffer, not just pointer
validity. Compute the expected extents from args.rows, args.cols,
args.group_size, and the supported bit width, including packed_per_row, and
reject undersized buffers before launch while preserving the existing input,
chunk, and output checks.
go/engine/hip/kernels/quant/affine_projection_validate.hipinc-46-47 (1)

46-47: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Size checks in this layer multiply two 32-bit fields before the widening operand. Each affected comparison has the form count_a * count_b * sizeof(T). C++ evaluates left to right, so count_a * count_b stays 32-bit and wraps at 2^32. Only then does sizeof(T) widen the result. A packet whose product wraps therefore passes the byte check against a much smaller allocation, and the kernels index past that allocation. go/engine/hip/kernels/sampling/diffusion_validate.hipinc lines 14-15 show the correct pattern for this layer.

  • go/engine/hip/kernels/quant/affine_projection_validate.hipinc#L46-L47: apply static_cast<uint64_t>(args.batch) to the input_bytes and output_bytes products in rocm_valid_mlx_q4_projection_batch_args.
  • go/engine/hip/kernels/quant/jangtq_codebook_validate.hipinc#L60-L61: apply static_cast<uint64_t> to args.codebook_count and args.code_count in rocm_valid_codebook_args.
  • go/engine/hip/kernels/quant/affine_projection_validate.hipinc#L88-L89: apply static_cast<uint64_t>(args.batch) to both products in rocm_valid_mlx_q4_projection_greedy_batch_args.

Note the PR states the reassembled source must match the original except for two banner comments. If you accept these fixes, update the source-comparison expectation in go/engine/hip/hip_kernel_source_test.go.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/quant/affine_projection_validate.hipinc` around lines
46 - 47, Prevent 32-bit multiplication overflow in the validation byte checks by
widening the first multiplicand before each product. In
go/engine/hip/kernels/quant/affine_projection_validate.hipinc:46-47, update both
products in rocm_valid_mlx_q4_projection_batch_args; in
go/engine/hip/kernels/quant/jangtq_codebook_validate.hipinc:60-61, widen
args.codebook_count and args.code_count in rocm_valid_codebook_args; and in
go/engine/hip/kernels/quant/affine_projection_validate.hipinc:88-89, update both
products in rocm_valid_mlx_q4_projection_greedy_batch_args. Also update the
source-comparison expectation in go/engine/hip/hip_kernel_source_test.go to
account for these accepted source changes.
go/engine/hip/kernels/quant/jangtq_codebook.hipinc-25-29 (1)

25-29: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Widen the packed element index to 64-bit.

Line 26 computes row * args.cols in 32-bit arithmetic. rocm_unpack_signed_bits then computes index * bits, also in 32-bit, at line 32 of go/engine/hip/kernels/quant/jangtq_codebook_validate.hipinc.

The bit offset wraps first. For bits == 8 the wrap starts once rows * cols exceeds about 5.4e8 elements, which is roughly 512 MiB of packed weights. After the wrap the kernel reads a valid but wrong byte, so the projection produces silently incorrect results rather than faulting.

The validator already widens for the same quantity. Line 6 of jangtq_codebook_validate.hipinc uses static_cast<uint64_t>(args.bits) * args.rows * args.cols. The kernel and the validator therefore disagree on the addressable range.

🐛 Proposed fix for the index arithmetic

Change the helper to take a 64-bit index:

-__device__ int8_t rocm_unpack_signed_bits(const uint8_t *packed, uint32_t bits, uint32_t index)
+__device__ int8_t rocm_unpack_signed_bits(const uint8_t *packed, uint32_t bits, uint64_t index)
 {
-	const uint32_t bit_offset = index * bits;
-	const uint32_t byte_index = bit_offset / 8u;
+	const uint64_t bit_offset = index * bits;
+	const uint64_t byte_index = bit_offset / 8u;
 	const uint32_t shift = bit_offset % 8u;

Then widen the caller:

 	for (uint32_t col = 0; col < args.cols; ++col) {
-		const uint32_t index = row * args.cols + col;
+		const uint64_t index = static_cast<uint64_t>(row) * args.cols + col;
 		const int8_t quantized = rocm_unpack_signed_bits(packed, args.bits, index);
 		sum += input[col] * static_cast<float>(quantized) * scale;
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/quant/jangtq_codebook.hipinc` around lines 25 - 29,
Widen packed-element index arithmetic in the quantization kernel: update
rocm_unpack_signed_bits and its callers to accept and compute with uint64_t, and
cast before multiplying row by args.cols in the kernel loop. Ensure the
subsequent index * bits calculation also remains 64-bit, matching the
validator’s addressable range.
go/engine/hip/kernels/sampling/diffusion.hipinc-501-505 (1)

501-505: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The output store has no per-dimension bounds check.

Line 503 writes dim_base + dim_lane for all four values. The only guard is dim_valid, set at Line 456 from dim_base < args.hidden_size. If args.hidden_size is not a multiple of ROCM_DIFFUSION_EXPECTED_EMBEDDING_Q8_G64_DIMS_PER_THREAD, the final thread writes up to three elements past the end of the row. On the last row that write leaves the output allocation.

rocm_diffusion_expected_embedding_q8_g64_dims4_rows4 performs the same tiled store and does check each dimension at Line 418. The same gap exists at Line 593 in rocm_diffusion_expected_embedding_q8_g64_subgroup32_rows64_prob4.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/sampling/diffusion.hipinc` around lines 501 - 505, Add
a per-dimension bounds check to the output stores in the affected kernels,
including rocm_diffusion_expected_embedding_q8_g64_dims4_rows4 and
rocm_diffusion_expected_embedding_q8_g64_subgroup32_rows64_prob4, so each write
occurs only when dim_base + dim_lane is less than args.hidden_size. Preserve the
existing accumulation and scaling behavior for valid dimensions.
go/engine/hip/kernels/quant/autoround.hipinc-19-22 (1)

19-22: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

packed_start_bit overflows 32 bits before the largest validated tensor.

value_start and packed_start_bit are uint32_t. rocm_valid_autoround_quantize_args computes packed_bits as uint64_t, so it accepts tensors whose total bit offset exceeds UINT32_MAX. At bits == 8, that limit is reached at about 536 million weights, which is roughly 2 GiB of float input. The validator accepts such a launch. packed_start_byte then wraps and the kernel writes to the wrong offset in packed.

Widen the offsets to 64-bit. rocm_autoround_pack_signed needs the same treatment for its index parameter.

🐛 Proposed fix: widen the offsets
-	const uint32_t value_start = row * args.cols + group * args.group_size;
-	const uint32_t packed_start_bit = value_start * args.bits;
-	const uint32_t packed_start_byte = packed_start_bit >> 3;
+	const uint64_t value_start = static_cast<uint64_t>(row) * args.cols + static_cast<uint64_t>(group) * args.group_size;
+	const uint64_t packed_start_bit = value_start * args.bits;
+	const uint64_t packed_start_byte = packed_start_bit >> 3;
 	const uint32_t packed_group_bytes = (args.group_size * args.bits + 7u) / 8u;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/quant/autoround.hipinc` around lines 19 - 22, Widen the
offset arithmetic in the autoround quantization kernel to 64-bit: update
value_start, packed_start_bit, packed_start_byte, and packed_group_bytes in the
shown launch-offset calculation, and change the index parameter of
rocm_autoround_pack_signed to the matching 64-bit type. Preserve the existing
offset formulas while ensuring multiplication, shifting, and indexing cannot
overflow before addressing packed data.
go/engine/hip/kernels/sampling/greedy.hipinc-39-75 (1)

39-75: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The tree reduction drops candidates when blockDim.x is not a power of two.

Line 39 accepts any threads value in the range 1 to 256. The reduction at Line 65 starts at stride = threads >> 1 and halves. That schedule only covers every slot when threads is a power of two.

Take threads == 6. The first pass uses stride == 3 and merges slots 3, 4, 5 into slots 0, 1, 2. The second pass uses stride == 1 and merges slot 1 into slot 0. Slot 2 is never merged. The candidate held by thread 2 is lost, and rocm_softcap_greedy_sample can return a token that is not the argmax.

Two fixes exist. Reject a non-power-of-two threads at Line 39, or round the first stride up to the next power of two and bound the partner index.

🐛 Proposed fix: handle a non-power-of-two thread count
-	for (uint32_t stride = threads >> 1; stride > 0; stride >>= 1) {
-		if (tid < stride) {
+	uint32_t reduction_width = 1u;
+	while (reduction_width < threads) {
+		reduction_width <<= 1u;
+	}
+	for (uint32_t stride = reduction_width >> 1; stride > 0; stride >>= 1) {
+		if (tid < stride && tid + stride < threads) {
 			const float other_score = scores[tid + stride];
 			const int32_t other_index = indices[tid + stride];
 			if (other_index >= 0 && (indices[tid] < 0 || other_score > scores[tid] || (other_score == scores[tid] && other_index < indices[tid]))) {
 				scores[tid] = other_score;
 				indices[tid] = other_index;
 			}
 		}
 		__syncthreads();
 	}

The alternative is a stricter launch check:

-	if (threads == 0 || threads > 256) {
+	if (threads == 0 || threads > 256 || (threads & (threads - 1u)) != 0u) {
 		return;
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/sampling/greedy.hipinc` around lines 39 - 75, Update
the tree reduction in rocm_softcap_greedy_sample so non-power-of-two threads are
handled correctly. Either reject non-power-of-two values in the existing threads
validation, or start reduction at the next power of two and guard partner
indices before reading scores and indices; preserve the argmax behavior for all
accepted thread counts.
go/engine/hip/kernels/quant/autoround.hipinc-26-33 (1)

26-33: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

The non-finite early return leaves the group output uninitialised.

If any weight in the group is not finite, the thread returns at Line 30. The thread then skips the scale store at Line 37, the byte clear at Lines 38-40, and the packing loop. The destination buffers keep their previous contents. The host receives a partially written result and has no status channel to detect it.

Two options exist. Write a deterministic sentinel for the group, or reject non-finite weights during validation. A deterministic sentinel keeps the kernel total.

🛡️ Proposed fix: write a deterministic zeroed group
 	float max_abs = 0.0f;
+	bool finite = true;
 	for (uint32_t offset = 0; offset < args.group_size; ++offset) {
 		const float value = weights[value_start + offset];
 		if (!isfinite(value)) {
-			return;
+			finite = false;
+			break;
 		}
 		max_abs = fmaxf(max_abs, fabsf(value));
 	}
+	if (!finite) {
+		scales[group_index] = 0.0f;
+		for (uint32_t byte = 0; byte < packed_group_bytes; ++byte) {
+			packed[packed_start_byte + byte] = 0;
+		}
+		return;
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/quant/autoround.hipinc` around lines 26 - 33, Update
the non-finite handling in the group-processing kernel around the max_abs scan
so it does not return before writing outputs. For any group containing a
non-finite weight, write a deterministic zero scale, clear the group’s
destination bytes, and skip packing while preserving normal processing for
finite groups.
go/engine/hip/kernels/serving/tiny_validate.hipinc-5-15 (1)

5-15: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

32-bit size products in the tiny validators allow out-of-bounds access in the tiny kernels. Both tiny validators compute element counts as uint32_t * uint32_t before promoting to size_t. The product wraps at 2^32, so the validator accepts a buffer that is far smaller than the kernel then addresses. rocm_valid_tiny_decode_args also computes (args.prior_token_count + 1u) in 32 bits, which wraps to zero at UINT32_MAX.

  • go/engine/hip/kernels/serving/tiny_validate.hipinc#L5-L15: cast one operand to uint64_t in the table_count product at line 5, and apply the same widening to the byte checks at lines 30, 35 and 36, and to lines 41, 67, 68, 69, 72, 73 and 74 in rocm_valid_tiny_decode_args.
  • go/engine/hip/kernels/serving/tiny.hipinc#L106-L119: no change is needed here once the validator computes the sizes in 64 bits. Confirm after the fix that updated_base at line 111 and the copy bound at line 107 stay inside the validated updated_key_bytes and updated_value_bytes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/serving/tiny_validate.hipinc` around lines 5 - 15,
Widen all tiny validator size arithmetic to 64 bits before multiplication or
addition: update table_count and the byte checks in rocm_valid_tiny_decode_args,
including the prior_token_count plus one calculation, by casting an operand to
uint64_t. In tiny.hipinc at lines 106-119, make no code changes; verify that
updated_base and the copy bound remain within the validator-produced
updated_key_bytes and updated_value_bytes after the widening fix.
go/engine/hip/kernels/serving/kv_descriptor_append.hipinc-178-190 (1)

178-190: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Distinguish a legitimate page skip from a trim failure.

rocm_kv_descriptor_trim_page returns false for two different reasons:

  1. The page ends at or before trim_start (line 48). The page holds no live tokens. Skipping it is correct.
  2. The page straddles trim_start but the encoding is not row-interleaved, or the byte accounting does not match the stride model (lines 56-69). This is an error. The retained tokens cannot be addressed.

Line 182 treats both cases as a skip. In case 2 the loop drops a page that still holds live tokens. The header written at lines 214-221 still reports args.output_token_count. The descriptor then claims more tokens than its pages describe.

The general fallback path at lines 375-391 already separates the two cases correctly. Line 378 skips the fully-trimmed page. Line 386 returns on a trim failure. Apply the same discrimination here.

Downstream, rocm_valid_device_kv_attention_descriptor in go/engine/hip/kernels/serving/kv_device.hipinc (lines 50-73) validates each page in isolation. It does not verify that the pages cover the token range without a gap. A descriptor with a dropped page passes that validation, so attention reads a cache with missing tokens and produces a wrong result with no error signal.

🐛 Proposed fix to separate the skip case from the failure case
 		for (uint32_t page_index = 0u; page_index < last_index; ++page_index) {
 			const rocm_device_kv_page_descriptor *page = reinterpret_cast<const rocm_device_kv_page_descriptor *>(previous_base + ROCM_DEVICE_KV_DESCRIPTOR_HEADER_BYTES + page_index * ROCM_DEVICE_KV_DESCRIPTOR_PAGE_BYTES);
+			if (page->token_start + page->token_count <= args.trim_start) {
+				continue;
+			}
 			rocm_device_kv_page_descriptor local_page {};
 			if (!rocm_kv_descriptor_trim_page(page, args.trim_start, &local_page)) {
-				continue;
+				return;
 			}
 			if (output_index >= output_last_index) {
 				return;
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/serving/kv_descriptor_append.hipinc` around lines 178 -
190, Update the page-processing loop around rocm_kv_descriptor_trim_page to
distinguish a fully trimmed page from a trim failure: skip only pages ending at
or before args.trim_start, but return immediately when a page straddles
trim_start and trimming fails. Preserve the existing output bounds check and
descriptor-page copying for successfully trimmed pages, matching the
discrimination used in the general fallback path.
go/engine/hip/kernels/serving/kv_device.hipinc-57-71 (1)

57-71: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

The validator does not verify that the pages cover the token range.

The loop checks each page in isolation. Line 60 bounds each page within args.token_count. No check compares the total covered tokens with args.token_count, and no check verifies that page N + 1 starts where page N ends.

Two malformed descriptors therefore pass validation:

  • A descriptor with a gap. The header reports token_count tokens, but the pages describe fewer. Attention then skips the missing tokens.
  • A descriptor with overlapping pages. Attention then attends to the same token twice.

Both produce a wrong result with no error signal. The append path in go/engine/hip/kernels/serving/kv_descriptor_append.hipinc at lines 178-190 can produce the gap case, so this is reachable.

Accumulate the covered token count in the loop, and compare it with args.token_count after the loop.

🛡️ Proposed fix to verify total coverage
 	const unsigned char *base = reinterpret_cast<const unsigned char *>(header);
+	uint64_t covered_tokens = 0;
 	for (uint32_t page_index = 0; page_index < header->page_count; ++page_index) {
 		const rocm_device_kv_page_descriptor *page = reinterpret_cast<const rocm_device_kv_page_descriptor *>(base + ROCM_DEVICE_KV_DESCRIPTOR_HEADER_BYTES + page_index * ROCM_DEVICE_KV_DESCRIPTOR_PAGE_BYTES);
 		if (page->token_count == 0 ||
 			page->token_start + page->token_count > args.token_count ||
 			return false;
 		}
+		covered_tokens += page->token_count;
 	}
+	if (covered_tokens != args.token_count) {
+		return false;
+	}
 	return true;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/serving/kv_device.hipinc` around lines 57 - 71, Update
the page-validation loop to accumulate each page’s covered token range and
require contiguous, non-overlapping pages by verifying each page starts at the
end of the previous page. After the loop, ensure the accumulated coverage equals
args.token_count, while preserving the existing per-page validation in the
descriptor validator.
go/engine/hip/kernels/serving/kv_descriptor_append.hipinc-429-462 (1)

429-462: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

The fused kernel can emit a descriptor that points at unencoded buffers.

This kernel runs three blocks. Blocks 0 and 1 encode the key and value tensors. Block 2 appends the page descriptor. Each block validates a different part of the packet:

  • Blocks 0 and 1 validate args.encode at line 440. If that check fails, they return without writing any encoded data.
  • Block 2 validates args.descriptor at line 458. If that check passes, it writes the page descriptor and the header.

The two validations are independent. If args.encode is invalid and args.descriptor is valid, block 2 publishes a descriptor whose key_pointer and value_pointer reference buffers that were never written. The header reports the new token count. The host observes a successful launch.

A later attention launch then reads uninitialised or stale KV data. rocm_valid_device_kv_attention_descriptor in go/engine/hip/kernels/serving/kv_device.hipinc checks only descriptor metadata, so it cannot detect this.

Validate both args.encode and args.descriptor in every block before any block writes. Each block then makes the same accept or reject decision.

🛡️ Proposed fix to make the accept decision uniform across blocks
 	if (args.version != ROCM_KV_ENCODE_TOKEN_VALUE_NORM_DESCRIPTOR_APPEND_LAUNCH_ARGS_VERSION ||
 		args.total_bytes != ROCM_KV_ENCODE_TOKEN_VALUE_NORM_DESCRIPTOR_APPEND_LAUNCH_ARGS_BYTES) {
 		return;
 	}
+	if (!rocm_valid_kv_encode_token_value_norm_args(args.encode) ||
+		!rocm_valid_kv_descriptor_append_args(args.descriptor)) {
+		return;
+	}
 	if (blockIdx.x < 2u) {
-		if (!rocm_valid_kv_encode_token_value_norm_args(args.encode)) {
-			return;
-		}
 		const bool encode_key = blockIdx.x == 0;
-	if (!rocm_valid_kv_descriptor_append_args(args.descriptor)) {
-		return;
-	}
 	rocm_kv_descriptor_append_execute(args.descriptor);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/serving/kv_descriptor_append.hipinc` around lines 429 -
462, Update rocm_kv_encode_token_value_norm_descriptor_append so every block
validates both args.encode and args.descriptor before performing any encoding or
descriptor append. Reuse the existing validation helpers, return immediately
when either check fails, and remove the block-specific validation split so all
three blocks make the same accept-or-reject decision.
go/engine/hip/kernels/serving/kv_encode.hipinc-129-134 (1)

129-134: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Non-power-of-two block launches can lose reduction results.

The strided reductions at the KV encode reduction sites fold the last active partial values in a loop with stride = threads >> 1. When blockDim.x is not a power of two, values can remain in lanes that are never folded into lane 0, so the emitted scale/rms can be too small and quantisation clamps those KV values. Use a power-of-two block size, or fold the remainder tail into lane 0 before this reduction pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/serving/kv_encode.hipinc` around lines 129 - 134,
Update the reduction logic in the KV encode kernel around the scratch-buffer
loop so non-power-of-two block sizes cannot leave partial values out of lane 0.
Either enforce a power-of-two block size for these launches or fold the
remainder tail into lane 0 before the existing strided reduction, while
preserving correct scale/RMS results for all supported block dimensions.
go/engine/hip/kernels/serving/kv_encode.hipinc-436-452 (1)

436-452: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The global-scale Q4 path drops elements when row_width is odd, and it rescans the whole tensor per row.

Two problems in this loop.

Correctness. packed_count at line 436 covers the whole tensor. The loop at line 440 walks that whole range for every row, and line 442 keeps only the indices inside the current row. first is always even, but row_start = row * row_width is odd for odd row_width on odd rows. The packed byte that straddles a row boundary is then handled by neither row correctly.

Take row_width == 3, row_count == 2, count == 6, so packed_count == 3:

  • Row 0 keeps first 0 and 2. For first == 2, dim == 2 and dim + 1 == 3 is not less than row_width, so the high nibble of payload[1] is written as zero.
  • Row 1 keeps only first == 4, giving dim == 1.
  • Element index 3, which is row 1 dimension 0, is never encoded. The high nibble of payload[1] stays zero.

The validator enforces an even width only for ROCM_DEVICE_KV_DESCRIPTOR_ENCODING_Q4_ROWS and ROCM_DEVICE_KV_DESCRIPTOR_ENCODING_Q4_ROWS_INTERLEAVED at lines 60-63. Plain ROCM_DEVICE_KV_DESCRIPTOR_ENCODING_Q4 has no parity guard, and nothing forbids row_count > 1 for it.

The equivalent path in rocm_encode_kv_token_tensor at lines 235-244 packs the flat stream and ignores row boundaries. That is correct for a single global scale, and it matches the size returned by rocm_device_kv_tensor_bytes at line 84 of go/engine/hip/kernels/serving/kv_device.hipinc.

Cost. Each row scans all packed_count indices and discards the ones outside the row. The loop performs row_count * packed_count iterations where packed_count would do.

Drive the loop from a flat packed index, and derive the row from it.

🐛 Proposed fix to pack the flat stream and remove the per-row rescan
 	const uint32_t packed_count = (count + 1u) / 2u;
 	for (uint32_t row = 0; row < row_count; ++row) {
 		const uint32_t row_start = row * row_width;
 		rocm_kv_encode_value_norm_prepare_row(input, row_start, head_dim, head_count, epsilon, scratch, head_rms);
-		for (uint32_t packed_index = tid; packed_index < packed_count; packed_index += threads) {
-			const uint32_t first = packed_index * 2u;
-			if (first < row_start || first >= row_start + row_width) {
-				continue;
-			}
-			const uint32_t dim = first - row_start;
-			const int low = rocm_quantize_kv_value(rocm_kv_encode_value_norm_value(input, row_start, dim, head_dim, head_rms), scale, encoding);
-			int high = 0;
-			if (dim + 1u < row_width) {
-				high = rocm_quantize_kv_value(rocm_kv_encode_value_norm_value(input, row_start, dim + 1u, head_dim, head_rms), scale, encoding);
-			}
-			payload[packed_index] = static_cast<unsigned char>(rocm_pack_signed_q4(low) | (rocm_pack_signed_q4(high) << 4));
-		}
+		const uint32_t row_packed_begin = row_start / 2u;
+		const uint32_t row_packed_end = (row_start + row_width + 1u) / 2u;
+		for (uint32_t packed_index = row_packed_begin + tid; packed_index < row_packed_end; packed_index += threads) {
+			const uint32_t first = packed_index * 2u;
+			int low = 0;
+			int high = 0;
+			if (first >= row_start && first < row_start + row_width) {
+				low = rocm_quantize_kv_value(rocm_kv_encode_value_norm_value(input, row_start, first - row_start, head_dim, head_rms), scale, encoding);
+			}
+			if (first + 1u >= row_start && first + 1u < row_start + row_width && first + 1u < count) {
+				high = rocm_quantize_kv_value(rocm_kv_encode_value_norm_value(input, row_start, first + 1u - row_start, head_dim, head_rms), scale, encoding);
+			}
+			payload[packed_index] = static_cast<unsigned char>(rocm_pack_signed_q4(low) | (rocm_pack_signed_q4(high) << 4));
+		}
 		__syncthreads();
 	}

A byte shared by two rows is still written twice under this fix. If plain Q4 with an odd row_width and multiple rows is not a supported shape, reject it in rocm_valid_kv_encode_token_value_norm_args instead:

+		(args.value_encoding != ROCM_DEVICE_KV_DESCRIPTOR_ENCODING_Q4 || row_count == 1u || (value_width & 1u) == 0u) &&
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/serving/kv_encode.hipinc` around lines 436 - 452,
Update the global-scale Q4 packing loop in rocm_encode_kv_token_value_norm to
iterate each flat packed_index once, derive the corresponding row and dimensions
from that index, and pack the flat tensor stream like
rocm_encode_kv_token_tensor rather than rescanning packed_count for every row.
If odd row_width with multiple rows cannot be safely represented because
boundary bytes would be written twice, reject that shape in
rocm_valid_kv_encode_token_value_norm_args.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: afafa3b6-c1af-4284-a04f-56ba9824bd47

📥 Commits

Reviewing files that changed from the base of the PR and between 2727995 and 17c64c4.

📒 Files selected for processing (62)
  • go/engine/hip/hip_kernel_source_test.go
  • go/engine/hip/kernels/README.md
  • go/engine/hip/kernels/activations/elementwise.hipinc
  • go/engine/hip/kernels/activations/elementwise_validate.hipinc
  • go/engine/hip/kernels/attention/attention_validate.hipinc
  • go/engine/hip/kernels/attention/chunked.hipinc
  • go/engine/hip/kernels/attention/device_kv.hipinc
  • go/engine/hip/kernels/attention/heads.hipinc
  • go/engine/hip/kernels/attention/rotary.hipinc
  • go/engine/hip/kernels/attention/rotary_validate.hipinc
  • go/engine/hip/kernels/attention/sdpa.hipinc
  • go/engine/hip/kernels/attention/single_head.hipinc
  • go/engine/hip/kernels/common/math.hipinc
  • go/engine/hip/kernels/common/numeric.hipinc
  • go/engine/hip/kernels/contract/launch_abi.hipinc
  • go/engine/hip/kernels/embedding/lookup.hipinc
  • go/engine/hip/kernels/embedding/lookup_validate.hipinc
  • go/engine/hip/kernels/embedding/mean_pool.hipinc
  • go/engine/hip/kernels/embedding/mean_pool_validate.hipinc
  • go/engine/hip/kernels/embedding/rerank.hipinc
  • go/engine/hip/kernels/embedding/rerank_validate.hipinc
  • go/engine/hip/kernels/lora/projection.hipinc
  • go/engine/hip/kernels/lora/projection_validate.hipinc
  • go/engine/hip/kernels/matmul/projection.hipinc
  • go/engine/hip/kernels/matmul/projection_validate.hipinc
  • go/engine/hip/kernels/moe/routing.hipinc
  • go/engine/hip/kernels/moe/routing_validate.hipinc
  • go/engine/hip/kernels/norms/rms_norm.hipinc
  • go/engine/hip/kernels/norms/rms_norm_fused_projection.hipinc
  • go/engine/hip/kernels/norms/rms_norm_fused_projection_validate.hipinc
  • go/engine/hip/kernels/norms/rms_norm_validate.hipinc
  • go/engine/hip/kernels/quant/affine_dot.hipinc
  • go/engine/hip/kernels/quant/affine_fused_validate.hipinc
  • go/engine/hip/kernels/quant/affine_gelu_tanh.hipinc
  • go/engine/hip/kernels/quant/affine_gelu_tanh_projection.hipinc
  • go/engine/hip/kernels/quant/affine_projection.hipinc
  • go/engine/hip/kernels/quant/affine_projection_fused.hipinc
  • go/engine/hip/kernels/quant/affine_projection_tuned.hipinc
  • go/engine/hip/kernels/quant/affine_projection_validate.hipinc
  • go/engine/hip/kernels/quant/autoround.hipinc
  • go/engine/hip/kernels/quant/autoround_validate.hipinc
  • go/engine/hip/kernels/quant/gguf_projection.hipinc
  • go/engine/hip/kernels/quant/gguf_validate.hipinc
  • go/engine/hip/kernels/quant/jangtq_codebook.hipinc
  • go/engine/hip/kernels/quant/jangtq_codebook_validate.hipinc
  • go/engine/hip/kernels/rocm_kernels.hip
  • go/engine/hip/kernels/sampling/diffusion.hipinc
  • go/engine/hip/kernels/sampling/diffusion_validate.hipinc
  • go/engine/hip/kernels/sampling/greedy.hipinc
  • go/engine/hip/kernels/sampling/greedy_validate.hipinc
  • go/engine/hip/kernels/sampling/topk.hipinc
  • go/engine/hip/kernels/sampling/topk_validate.hipinc
  • go/engine/hip/kernels/serving/kv_descriptor_append.hipinc
  • go/engine/hip/kernels/serving/kv_device.hipinc
  • go/engine/hip/kernels/serving/kv_encode.hipinc
  • go/engine/hip/kernels/serving/prefill_decode.hipinc
  • go/engine/hip/kernels/serving/prefill_decode_validate.hipinc
  • go/engine/hip/kernels/serving/tiny.hipinc
  • go/engine/hip/kernels/serving/tiny_validate.hipinc
  • go/engine/hip/kernels/serving/tiny_weights.hipinc
  • go/engine/hip/kernels/training/losses.hipinc
  • go/engine/hip/kernels/training/losses_validate.hipinc

Comment on lines +3 to +26
__device__ float rocm_block_reduce_sum(float value, float *scratch)
{
const uint32_t lane = threadIdx.x & 31u;
const uint32_t wave = threadIdx.x >> 5u;
const uint32_t wave_count = (blockDim.x + 31u) >> 5u;
for (uint32_t stride = 16u; stride > 0u; stride >>= 1u) {
value += rocm_shfl_down(value, stride, 32);
}
if (lane == 0u) {
scratch[wave] = value;
}
__syncthreads();
float reduced = lane < wave_count ? scratch[lane] : 0.0f;
if (wave == 0u) {
for (uint32_t stride = 16u; stride > 0u; stride >>= 1u) {
reduced += rocm_shfl_down(reduced, stride, 32);
}
if (lane == 0u) {
scratch[0] = reduced;
}
}
__syncthreads();
return scratch[0];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Add a barrier after the final scratch[0] read to make repeated calls safe.

rocm_block_reduce_sum ends with a read of scratch[0] at line 25. The last barrier is at line 24, before that read. When the same scratch buffer is reused by a second call in the same kernel, the first write of the second call (scratch[wave] = value, line 12) has no barrier between it and the pending line 25 read of the first call. A fast thread can overwrite scratch[0] while a slow thread is still reading the previous total.

This affects rocm_moe_combine_norms (lines 160-161) and rocm_rms_norm_residual_add_norm (lines 193 and 208), which both call the reduction twice with the same partial array. The result is a non-deterministic wrong RMS value.

🐛 Proposed fix: read into a register, then synchronise
 	__syncthreads();
-	return scratch[0];
+	const float result = scratch[0];
+	__syncthreads();
+	return result;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
__device__ float rocm_block_reduce_sum(float value, float *scratch)
{
const uint32_t lane = threadIdx.x & 31u;
const uint32_t wave = threadIdx.x >> 5u;
const uint32_t wave_count = (blockDim.x + 31u) >> 5u;
for (uint32_t stride = 16u; stride > 0u; stride >>= 1u) {
value += rocm_shfl_down(value, stride, 32);
}
if (lane == 0u) {
scratch[wave] = value;
}
__syncthreads();
float reduced = lane < wave_count ? scratch[lane] : 0.0f;
if (wave == 0u) {
for (uint32_t stride = 16u; stride > 0u; stride >>= 1u) {
reduced += rocm_shfl_down(reduced, stride, 32);
}
if (lane == 0u) {
scratch[0] = reduced;
}
}
__syncthreads();
return scratch[0];
}
__device__ float rocm_block_reduce_sum(float value, float *scratch)
{
const uint32_t lane = threadIdx.x & 31u;
const uint32_t wave = threadIdx.x >> 5u;
const uint32_t wave_count = (blockDim.x + 31u) >> 5u;
for (uint32_t stride = 16u; stride > 0u; stride >>= 1u) {
value += rocm_shfl_down(value, stride, 32);
}
if (lane == 0u) {
scratch[wave] = value;
}
__syncthreads();
float reduced = lane < wave_count ? scratch[lane] : 0.0f;
if (wave == 0u) {
for (uint32_t stride = 16u; stride > 0u; stride >>= 1u) {
reduced += rocm_shfl_down(reduced, stride, 32);
}
if (lane == 0u) {
scratch[0] = reduced;
}
}
__syncthreads();
const float result = scratch[0];
__syncthreads();
return result;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/norms/rms_norm.hipinc` around lines 3 - 26, Update
rocm_block_reduce_sum so it reads scratch[0] into a local register after the
existing final barrier, adds a __syncthreads() before returning that register,
and thereby prevents a subsequent call from overwriting the shared buffer while
another thread still reads it. Preserve the reduction and return value behavior
for callers such as rocm_moe_combine_norms and rocm_rms_norm_residual_add_norm.

Comment on lines +222 to +226
const uint32_t block_lane = threadIdx.x & (ROCM_Q8_1_BLOCK_SIZE - 1u);
const uint32_t workgroup_block = threadIdx.x / ROCM_Q8_1_BLOCK_SIZE;
const uint32_t blocks_per_workgroup = blockDim.x / ROCM_Q8_1_BLOCK_SIZE;
const uint32_t block_count = args.count / ROCM_Q8_1_BLOCK_SIZE;
for (uint32_t block_base = 0; block_base < block_count; block_base += blocks_per_workgroup) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Guard against blockDim.x < ROCM_Q8_1_BLOCK_SIZE to prevent an infinite loop.

Line 224 computes blocks_per_workgroup = blockDim.x / ROCM_Q8_1_BLOCK_SIZE with integer division. If the host launches this kernel with fewer than ROCM_Q8_1_BLOCK_SIZE threads per block, blocks_per_workgroup is 0. The loop at line 226 then never advances block_base, and block_count is at least 1 because the validator requires count > 0 and count % ROCM_Q8_1_BLOCK_SIZE == 0. The kernel hangs the device.

Other kernels in this layer already reject an unexpected block size. See rocm_projection at go/engine/hip/kernels/matmul/projection.hipinc line 15.

🛡️ Proposed fix
 	rocm_q8_1_block *q8_output = reinterpret_cast<rocm_q8_1_block *>(static_cast<uintptr_t>(args.q8_output_pointer));
+	if (blockDim.x < ROCM_Q8_1_BLOCK_SIZE) {
+		return;
+	}
 	const uint32_t block_lane = threadIdx.x & (ROCM_Q8_1_BLOCK_SIZE - 1u);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/norms/rms_norm.hipinc` around lines 222 - 226, Guard
the kernel entry path before the `block_base` loop so launches with `blockDim.x
< ROCM_Q8_1_BLOCK_SIZE` return immediately, preventing `blocks_per_workgroup`
from being zero. Keep the existing loop unchanged for valid block sizes, and
follow the early block-size rejection pattern used by `rocm_projection`.

Comment on lines +22 to +38
return args.version == ROCM_AUTOROUND_QUANTIZE_LAUNCH_ARGS_VERSION &&
args.total_bytes == ROCM_AUTOROUND_QUANTIZE_LAUNCH_ARGS_BYTES &&
args.weight_pointer != 0 &&
args.packed_pointer != 0 &&
args.scale_pointer != 0 &&
args.rows > 0 &&
args.cols > 0 &&
args.group_size > 0 &&
args.cols % args.group_size == 0 &&
args.groups_per_row == args.cols / args.group_size &&
rocm_valid_autoround_format(args.format_code, args.bits) &&
args.weight_bytes == value_count * sizeof(float) &&
args.packed_bytes == (packed_bits + 7u) / 8u &&
args.scale_bytes == scale_count * sizeof(float) &&
args.nsamples > 0 &&
args.seqlen > 0 &&
args.iters > 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Groups that are not byte aligned cause a write race in rocm_autoround_quantize.

The shared root cause is a missing constraint in rocm_valid_autoround_quantize_args. The validator accepts any group_size that divides cols, and rocm_valid_autoround_format allows bits of 2, 4 or 8. It never requires (group_size * bits) % 8 == 0.

One thread owns one group. When group_size * bits is not a multiple of 8, two adjacent groups share a byte in packed. Two threads then touch the same byte with no atomics. Take bits == 4 with group_size == 3 and cols == 3: each group covers 12 bits, so group 1 starts mid-byte. Thread 1 clears the shared byte after thread 0 has already packed into it, and both threads perform a non-atomic read-modify-write OR. The packed output is corrupt and the result is not reproducible.

  • go/engine/hip/kernels/quant/autoround_validate.hipinc#L22-L38: add (args.group_size * args.bits) % 8 == 0 to the validation chain. That makes every group start and end on a byte boundary, so packed_group_bytes covers exactly the owned range.
  • go/engine/hip/kernels/quant/autoround.hipinc#L38-L49: no change is needed once the constraint is added. The byte clear at Lines 38-40 and the OR in rocm_autoround_pack_signed then stay inside the region owned by the thread.
🔒 Proposed fix in the validator
 		args.group_size > 0 &&
 		args.cols % args.group_size == 0 &&
+		(args.group_size * args.bits) % 8 == 0 &&
 		args.groups_per_row == args.cols / args.group_size &&
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return args.version == ROCM_AUTOROUND_QUANTIZE_LAUNCH_ARGS_VERSION &&
args.total_bytes == ROCM_AUTOROUND_QUANTIZE_LAUNCH_ARGS_BYTES &&
args.weight_pointer != 0 &&
args.packed_pointer != 0 &&
args.scale_pointer != 0 &&
args.rows > 0 &&
args.cols > 0 &&
args.group_size > 0 &&
args.cols % args.group_size == 0 &&
args.groups_per_row == args.cols / args.group_size &&
rocm_valid_autoround_format(args.format_code, args.bits) &&
args.weight_bytes == value_count * sizeof(float) &&
args.packed_bytes == (packed_bits + 7u) / 8u &&
args.scale_bytes == scale_count * sizeof(float) &&
args.nsamples > 0 &&
args.seqlen > 0 &&
args.iters > 0;
return args.version == ROCM_AUTOROUND_QUANTIZE_LAUNCH_ARGS_VERSION &&
args.total_bytes == ROCM_AUTOROUND_QUANTIZE_LAUNCH_ARGS_BYTES &&
args.weight_pointer != 0 &&
args.packed_pointer != 0 &&
args.scale_pointer != 0 &&
args.rows > 0 &&
args.cols > 0 &&
args.group_size > 0 &&
args.cols % args.group_size == 0 &&
(args.group_size * args.bits) % 8 == 0 &&
args.groups_per_row == args.cols / args.group_size &&
rocm_valid_autoround_format(args.format_code, args.bits) &&
args.weight_bytes == value_count * sizeof(float) &&
args.packed_bytes == (packed_bits + 7u) / 8u &&
args.scale_bytes == scale_count * sizeof(float) &&
args.nsamples > 0 &&
args.seqlen > 0 &&
args.iters > 0;
📍 Affects 2 files
  • go/engine/hip/kernels/quant/autoround_validate.hipinc#L22-L38 (this comment)
  • go/engine/hip/kernels/quant/autoround.hipinc#L38-L49
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/hip/kernels/quant/autoround_validate.hipinc` around lines 22 - 38,
Update rocm_valid_autoround_quantize_args in
go/engine/hip/kernels/quant/autoround_validate.hipinc:22-38 to require
(args.group_size * args.bits) % 8 == 0 alongside the existing group-size checks.
This ensures each packed group occupies whole bytes; rocm_autoround_quantize and
rocm_autoround_pack_signed in go/engine/hip/kernels/quant/autoround.hipinc:38-49
require no direct changes because the validator constraint prevents overlapping
byte access.

Snider added a commit that referenced this pull request Aug 6, 2026
#23)

* refactor(engine/metal): the kernel pile takes the family shape hip set

engine/hip's kernels/ was restructured into the QuixiCore family taxonomy
(PR #22); engine/metal was a flat pile of 47 .metal files + 2 shared _impl.h
in one directory. Same treatment, same vocabulary — the two engines now read
alike:

  activations/ 5  attention/ 14  embedding/ 4  megakernel/ 4  moe/ 6+2h
  norms/ 3  probes/ 2  quant/ 7  sampling/ 1  training/ 1  experimental/ 1

Unlike the HIP amalgamation TU, this is a SORT, not a split: every file moved
byte-identically (git records all 49 as R100; per-file sha256 old->new all
match), no shader edited, no file or kernel function renamed. Classification
is by what the kernel does, not by its filename prefix — lthn_moe_combine_norms
is the MoE block's norm/combine tail and sits in norms/ exactly as
rocm_moe_combine does on engine/hip; lthn_gather_qmv + lthn_gelu_qmv are the
affine expert-route projections and sit in moe/ (which also keeps each shared
_impl.h beside its consumers, since the quoted #include resolves relative to
the includer).

Two families have no HIP counterpart and are named for what they are:
megakernel/ (the grid-barrier whole-stage dispatches, an Apple-Silicon lever)
and probes/ (grid-sync + cross-threadgroup coherency, compiled into the
library because the tests dispatch them). contract/ and lora/ do not exist
here; common/ and matmul/ are absent by observation — there is no engine-wide
numeric header, and every projection kernel here is quantised.

Build glue: task metallib:kernels now collects sources RECURSIVELY over
kernels/, excluding experimental/ (which was never in the shipped library and
still is not) — .metal files are independent compilation units linked into one
metallib, so the tree carries no ordering meaning and a new family directory
needs no build change. A duplicate-basename guard fails the build loudly: the
.air objects are named by basename, so two families holding one filename would
silently drop a kernel.

Receipts, this Mac (M-series, MetalToolchain v17.6, macOS 26):
  - 47 sources before, 47 after; no dupes.
  - lthn_kernels.metallib is REPRODUCIBLE: the baseline rebuild reproduced the
    2026-07-22 artefact byte for byte (sha256 6be8385f0b63f79d…762207d), so the
    metallib hash is a usable no-regression proof rather than a moving target.
  - 209 exported functions via `xcrun metal-nm` — the parity instrument, and
    the reason kernels/README.md carries no hand-maintained symbol list.

Co-Authored-By: Virgil <virgil@lethean.io>

* docs(engine/metal): point the Go comment references at the family they moved to

26 doc comments across 17 files name a kernel source by path
(kernels/lthn_tq_kv.metal, "compiled from kernels/*.metal", …). Nothing on the
Go side READS those paths — the engine resolves kernels by function name off
the loaded library (customLibrary.NewFunctionWithName), which is why the move
needed no launcher change — but "path is documentation" only holds while the
path is true.

Comment text only; substitution was driven by the exact old->new table the move
itself produced, and every changed line was read back in the diff. go build +
go vet green.

Co-Authored-By: Virgil <virgil@lethean.io>
Snider added a commit that referenced this pull request Aug 8, 2026
…d on Windows) (#30)

v0.12.1 carries the two core/go fixes this repo's windows lane has been
blocked on, and which no amount of work here could reach:

- #21 — io.Local was local.New("/"), and "/" is not an absolute root on
  Windows, it names the CURRENT DRIVE. An absolute "C:\Users\..." under a
  D:-rooted medium gave "path escapes from parent"; a relative
  "testdata/x.json" resolved against the drive root instead of the cwd and
  gave "The system cannot find the path specified". 30 of the 34 remaining
  packages report one shape or the other.
- #22 — App.Find carried the same three defects go-process v0.16.2 fixed:
  no %PATHEXT% expansion, a mode&0111 test Windows can never satisfy (os.Stat
  synthesises 0666/0444, so it rejected every file including git.exe), and a
  path-vs-name check that missed '/'. 3 packages report
  `Program.Find: "git": not found in PATH`.

Receipts — macOS:
  GOWORK=off go test -count=1 ./...   exit=0, 176 packages ok
  GOWORK=off go build ./... · go vet ./...   clean
  cli:      go test -count=1 ./tui/...   ok 6.510s
  examples: go build ./... · go vet ./... clean

Only go/ is bumped. cli/ pins v0.12.0 and examples/ v0.11.0, but both build
through the workspace, which already selects the highest version across
modules — the skew predates this and is not widened by it.

Lane state going in: 34 (main, run 31251799117). The windows number is this
lane's own and is posted from the run, not predicted here.
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