Repository navigation
cuda: RMS norm rows and token rows for every family, Nemotron's embed the first user - #736
Conversation
…rows.cu) Shared CUDA modules with float64 references in core (norms_ref, act_ref): rms, the residual step, add_rms_h (a bf16 residual stream with 64-group sums), gated head norms, and token rows from 4-bit affine (MLX layout) or bf16 tables. tf-cuda-test norms and norm-rows check them against float64 and every row count's bits against the widest.
…when the CUDA server uses its own kernels --kernels native is the CUDA CLI's flag; the server runs its own kernels whenever it has no captured Triton set.
|
Thank you, @jschmied. Shared row kernels with a float64 reference and a row-count check each are what the next CUDA family needs, and moving Nemotron's 4-bit embed over first, with the same bits, gives them a careful first user. Both commits landed in 1.0.9 under your name, One change on our side, in a commit after yours ( What we measured on a GB10 (sm_121), against the build before your change:
Notes for whoever moves a family onto the shared norms. None of them blocks this PR.
|
What this changes
Nearly every family needs the same row kernels around its projections: RMSNorm, the residual add with its norm, a gated head norm (the Gated DeltaNet families), and token rows from the embedding table. Today each CUDA family writes its own. Nemotron has
nemotron_norms.cu, #608's 27B runs Triton for the same steps, and the next family would write them a third time.This adds them as shared modules, built the way the other
cuda/modules are: each has a float64 reference incore, atf-cuda-testcommand, the same bits at any row count, and a mutation the test catches.norms.cu/cuda/norms.zig:rms(f32, f16 or bf16 in and out, optional weights), the residual step (add_rms),add_rms_h(a bf16 residual stream: h = x + r, y = rmsnorm(h) · w, plus y's 64-value group sums for a 4-bit projection) andgated_rms(rmsnorm(y) · w · silu(z) per head, from strided rows). One block per row, sums in a fixed order. Rows up to 8,192 wide stay in registers.embed_rows.cu/cuda/embed_rows.zig: token rows from a 4-bit affine table (MLX layout, groups of 64) or a bf16 table, 16 bytes per thread.core/norms_ref.zigandcore/act_ref.zig: the float64 references.Nemotron is the first user: its 4-bit embed now runs on
embed_rows, andtf_nemo_embedis removed. It gives the same bits, since both computefmaf(q, scale, bias)per value.Two things are deliberately left out:
tf_nemo_embed16, ModelOpt checkpoints). I have no ModelOpt Nemotron checkpoint to run end to end.Receipt
0efbd447(1.0.8) plus these two commits; one DGX Spark (GB10, sm_121, driver 580.173.02, CUDA 13);TensorFold/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-MLX-4bit.tf-cuda-test normsandnorm-rows: every output is within its float64 tolerance (worst 0.996 of a tolerance), and fewer rows (1, 3, 17) give the same bits as the most (300 or 64).tf-cuda-test glue: 100 of 100 Nemotron glue comparisons byte-equal, the embed now throughembed_rows.tensorfold laneswith--kernels native(our glue, not a captured Triton set), 6 prompts of 5 to 5,000 tokens, 160 tokens each. Drafted,--no-draftsand--sologive the same token hashes on0efbd447and on this branch: 18 of 18 streams equal.normsandnorm-rows. A 4-bit row reading the low nibble twice failsnorm-rowsandglue.0efbd447first, then this branch). The embed itself is about 3× faster per call in an in-model profile (median 5.98 → 1.90 µs), but there are only 46 calls a run.zig build testandzig build test-cuda-host(44/44 steps, 619 passed, 5 skipped as onmain).lean_check.py: 539 findings, as onmain.Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. (Served hashes equal across all three modes and both builds.)