Skip to content

MiniMax H3 / FastH3 on CUDA in the checkpoint's bf16, one GPU or several Sparks over RoCE - #958

Closed
drowzeys wants to merge 13 commits into
ashhart:mainfrom
drowzeys:upstream/h3-cuda-bf16
Closed

drowzeys wants to merge 13 commits into
ashhart:mainfrom
drowzeys:upstream/h3-cuda-bf16

Conversation

@drowzeys

Copy link
Copy Markdown
Contributor

Thank you, Ash, for the review of #585 and #947, and for the runtime this builds on.

H3's 50 blocks now run on TensorFold's CUDA runtime at the checkpoint's own precision, and the same pass splits across DGX Sparks with the bits of one GPU.

  • bf16 weights as shipped, bf16 activations, fp32 sums, fp32 softmax and fp32 routing.
  • The products are the runtime's own cuBLASLt Linear (zig/src/cuda/cublaslt.zig: fp32 compute, no TF32).
  • Ranks trade bf16 keys and values.
  • Nothing is quantized, and int8 is gone from the family.

This answers #585's objection ("the int8 projections and attention are also a precision trade we don't make anywhere in the engine"). It is the CUDA half of #949, the Metal bf16 version: the same routing in fp32, the same kernel set, and its float64 references ported. The two read as one family on both backends.

The receipts are for the same machines and clip as #947: a 5 s clip at 864x480, 124 frames, 15,433 rows and 8 passes, on DGX Spark (GB10). The reference is ComfyUI's own bf16 blocks.

  • Parity, far tighter than int8. All 50 blocks, dense, first pass, against ComfyUI's bf16 blocks on the same rows: cosine 0.99910, relative error 0.042. int8 (MiniMax H3 / FastH3 video and audio generation on CUDA, one GPU or several Sparks over RoCE #947) was 0.99669 and 0.081. One block: cosine 0.99999, relative error 0.0032 (int8 0.0150).
  • Closer to an fp32 forward than the reference itself. An independent fp32 implementation of the blocks, on the same rows, puts this library at relative error 0.0354 after 50 blocks and ComfyUI's bf16 blocks at 0.0445.
  • Bit-identical across ranks. 8 of 8 pass streams match by sha256 at 1, 2 and 3 Sparks, and again after a restart.
  • Speed, per pass:
  • Kernel contract: every H3 kernel registered. 11 of 11 H3 kernels pass gates 1–4, including the routing, norm, head, pool, top-k and mix kernels that MiniMax H3 / FastH3 video and audio generation on CUDA, one GPU or several Sparks over RoCE #947 left out. 0 limit problems on sm_80, 86, 89, 120 and 121.
  • Lean check: 517 on main, 517 here. zig build test passes.
  • A rendered clip matches the int8 and reference clips by eye. Whisper hears the spoken line in all three.

Stacked on #947; the commits of this change alone are drowzeys/TensorFold@upstream/h3-cuda-parallel...upstream/h3-cuda-bf16. If you would rather take one change, the two squash cleanly into a bf16-only family. The int8 variant stays on the fork.

Why bf16 only, and not bf16 beside int8

  • It is the precision the engine keeps everywhere, and what CONTRIBUTING asks for: the checkpoint's activation precision and fp32 accumulation.
  • One path means one set of kernels to review and keep under the contract. The int8 products, the quantizer and the saved .tf-int8 copy are gone: 987 lines out, 497 in, in the family and its kernels.
  • MiniMax H3 / FastH3 video and audio generation on Metal, in the checkpoint's bf16 #949 makes the same choice on Metal.
  • int8's only advantage is speed. The multi-Spark split recovers that without a precision trade.

What changed

  • zig/src/families/h3/:
    • cuda_weights.zig copies the checkpoint's bf16 block tensors to the device as stored (42 GB). tf_h3_set_weight copies a host-merged bf16 projection in.
    • cuda_gemm.zig (new) holds the products on the runtime's Linear. Each (outputs, inputs) shape keeps the algorithm cuBLASLt picks at 16,384 rows, so a rank's rows sum in the same order as on one GPU. That is what keeps ranks bit-identical.
    • cuda.zig holds the forward. The dense pass skips the routing gate's product.
    • cuda_tp.zig handles the exchange: bf16 keys and values, plus fp32 key and value means when routing.
  • zig/kernels/cuda/h3.cu, h3_attention.cuh:
    • The rows: modulated norm, gated adds, SwiGLU and head layout. They round to bf16 where torch's bf16 ops round in the reference.
    • Routing in fp32: tile means, tile scores, top-k and the pooled branch.
    • The tile-routed attention on mma.sync m16n8k16: bf16 q, k and v, fp32 scores and online softmax. The weights are rounded to bf16 for the value product, as flash attention does. Q, K and V take 48 KiB, so two blocks fit on an SM, and V's load overlaps the scores.
  • zig/tests/cuda/contract_h3*.zig: nine kernels against their float64 formulas (the Metal family's references, ported), and the top-k against a CPU oracle under a stated rule: a NaN score ranks as −inf, and ties go to the lower tile. h3_spans is as before. registry.zig holds 11 records.
  • docs/recipes/minimax-h3-cuda.md: the arithmetic and the receipt.

The library's C API is unchanged except that tf_h3_open takes the checkpoint alone; there is no saved copy. The lane core and the server are untouched.

Receipts

Environment:

  • DGX Spark: GB10, cc 12.1, driver 580.173.02, CUDA 13.0.88, cuBLASLt 13.1.1.3, Ubuntu 24.04.4, kernel 6.17.0-1029-nvidia.
  • This branch at 96f0aa2e, on MiniMax H3 / FastH3 video and audio generation on CUDA, one GPU or several Sparks over RoCE #947's b570b8f0, on main 46a08c18.
  • Host ComfyUI a4b5a045 (0.39.0).
  • Checkpoint FastVideo/FastVideo-FastH3-Comfy fastvideo_fasth3_8step_v2_pruned_bf16.safetensors.
  • 864x480, 124 frames, 8 passes, sparsity 0.8, seeds 7 (gate) and 8 (timed).
  • Rank 0 is the same machine for every row; ranks talk over RoCE on two rails.
  • One model process per GPU, under a MemAvailable kill guard.
Blocks A pass Sampler, 8 passes Clip
ComfyUI's bf16 blocks, dense (reference, from #947) 10.6 s 85.1 s 103.0 s
ComfyUI's bf16 blocks, its sparse attention 0.8 11.1 s 92.8 s 111.4 s
#947 int8, 1 GPU (rerun today) 6.61–6.72 s 54.0 s 72.4 s
This library, 1 GPU 10.27–10.41 s 82.8 s 101.1 s
This library, 2 GB10s 6.26–6.42 s 51.2 s 69.4 s
This library, 3 GB10s 4.92–5.06 s 40.3 s 58.5 s

For comparison, #947's int8 on 2 and 3 GB10s: 4.06–4.14 s and 3.02–3.06 s a pass. bf16 doubles the bytes exchanged, about 1.0 s of a 3-rank pass against int8's 0.6 s.

Where one GPU's pass goes, profiled on the library alone:

Stage s Stage s
q, k, v and gate product 2.53 attention out product 0.69
head layout 0.48 MLP in (product + SwiGLU) 2.87
routing 0.31 MLP out product 1.42
attention 1.47 norms, gate mix 0.48

Bit-identity:

Check Result
2 ranks against 1, 8 passes, seed 7 8 of 8 pass streams sha256-identical
3 ranks against 1 8 of 8 identical
1 GPU after a ComfyUI restart 8 of 8 identical

Parity. All rows are measured on the first pass's rows with dense attention. The case was dumped from ComfyUI and replayed through each library on the same machine. Errors are relative L2. Worst row and worst column are the relative errors of the worst single row and channel.

Against Blocks Cosine Rel. error Max abs (of values up to) Worst row Worst column
ComfyUI bf16 1, this 0.99999 0.0032 256 (1.08e5) 0.020 0.015
ComfyUI bf16 1, int8 #947 0.99989 0.0150 512 0.077 0.128
ComfyUI bf16 50, this 0.99910 0.0424 2.3e5 (6.2e6) 0.547 0.220
ComfyUI bf16 50, int8 #947 0.99669 0.0813 7.9e5 0.561 0.897
fp32 forward 1, ComfyUI bf16 0.99999 0.0052 558 0.022 0.026
fp32 forward 1, this 0.99999 0.0050 558 0.016 0.025
fp32 forward 50, ComfyUI bf16 0.99901 0.0445 1.3e5 0.328 0.228
fp32 forward 50, this 0.99937 0.0354 2.9e5 0.341 0.178
fp32 forward 50, int8 #947 0.99714 0.0756 7.3e5 0.382 0.887

The fp32 forward is an independent torch implementation of the blocks: fp32 weights and activations, no TF32, SDPA's memory-efficient fp32 kernel. It shares no code with ComfyUI or this library. Against it, this library's aggregate and worst-column errors are below ComfyUI's own. Its worst row is slightly above (0.341 against 0.328).

Memory, the library alone: tf_h3_open on the checkpoint, then three passes over the 15,433-row geometry, sampled every 0.5 s.

Device (nvidia-smi peak) MemAvailable drop
1 GPU, every tile 44,253 MiB (bf16 block weights 40,426) 45,746 MiB
1 of 2 ranks, 144 tiles, 3 GiB region 42,870 MiB 47,322 MiB

int8 needed 23,052 MiB. The process maps the checkpoint while it copies the tensors, so its resident pages peak at 36–38 GB during the load. It ran beside ComfyUI with its own checkpoint mapped and its text encoder resident, on a 128 GB Spark.

Clips (seed 7 fox, frames at 2.5 s and 4 s; and a spoken line, seed 11). Left to right: this library, #947 int8, ComfyUI's bf16 blocks. All three are clean and coherent, and differ only as sparse routing does. Whisper (small, English) heard "The last train leaves at nine tonight." from all three speech clips.

Fox, left to right: bf16 here, #947 int8, ComfyUI bf16

Spoken line, same order

Commands:

zig build tf-h3 -Doptimize=ReleaseFast -Dnvcc=/usr/local/cuda/bin/nvcc
zig build kernel-contract -Dnvcc=/usr/local/cuda/bin/nvcc
zig build fatbins kernel-contract-fatbins -Dnvcc=/usr/local/cuda/bin/nvcc -Dsm=80,86,89,120,121
tf-cuda-test kernel-contract-limits zig-out/fatbin

The clip timings came from the Studio's ComfyUI node, as in #947. Parity used a case dump of the first pass, replayed through each library's tf_h3_forward_host. The ranks ran the Studio's rank.py, and the memory figures came from its bench.py. These are app-side harnesses and are not part of this change.

Kernel contract

SUMMARY 23 kernels: 23 pass gates 1-4, 0 fail, 0 known findings; 5 controls: 5 red as planted, 0 missed; 0 limit problems; gates 47300 ms, limits 1 ms
PASS kernel-contract-limits: every registered launch fits sm_80, sm_86, sm_89, sm_120 and sm_121, every control red

The H3 kernels are h3_norm, h3_gate_add, h3_gate_mix, h3_swiglu, h3_heads, h3_pool, h3_tile_scores, h3_coarse, h3_attention, h3_topk and h3_spans.

  • Each is checked against its formula in float64.
  • Intermediate bf16 roundings are declared as mids, and inputs are one-signed where a sum could cancel.
  • Gate 4 is exact for h3_tile_scores. The softmax, norm and pool kernels state why their output hides the sums' error, as main's softmax and RMS norm do.
  • Gate 3 runs at the engine's widths (5,376 and 14,336), and at 2 heads and 16 tiles for the attention and routing kernels.
  • The head-layout test keeps the rotary sine small against the cosine. A channel and its partner share one table entry, and a cancelling pair would hide an intermediate rounding from the bound.
  • Gates 1–4 ran on GB10; gate 5 covers all five targets.

Checks

Check main 46a08c1 #947 This branch
python3 tools/zig/lean_check.py 517 517 517
zig build test (fresh cache) 84/84 steps, 1376 passed, 7 skipped 86/86, 1378 passed, 7 skipped 86/86 steps, 1378 of 1385 passed, 7 skipped, 0 failed
zig build test-kernel-contract 18/18 18/18 18/18
zig fmt --check, changed Zig files clean
Files ≤ 600 lines largest 324

Gaps and next steps

  • An FP64 reference per projection class. The products are upstream's Linear, unchanged. Its one change of use is a fixed algorithm per shape. This change checks them end to end against the fp32 forward, not class by class in FP64. I can add a per-class FP64 check of q/k/v, out, fc1 and fc2 on the engine's own rows if you want it in this PR.
  • One-GPU speed. The products are about 73% of a pass and already near the GB10's bf16 rate. The attention (1.47 s) and the head layout (0.48 s, 255 registers) have room left.
  • The exchange. bf16 doubles the bytes. Overlapping the exchange with the next block's norm and products would hide most of the 1.0 s a 3-rank pass spends in it.
  • How a denoiser fits the engine. This is unchanged from MiniMax H3 / FastH3 video and audio generation on CUDA, one GPU or several Sparks over RoCE #947, and it is the same question as MiniMax H3 / FastH3 video and audio generation on Metal, in the checkpoint's bf16 #949's.
  • Not yet tested:
    • GPUs other than GB10;
    • checkpoints other than FastH3 8-Step V2;
    • sizes other than 864x480;
    • four or more ranks;
    • recovery when a rank dies (today the pass fails).

Licenses

The code is Apache-2.0. MiniMax H3 / FastH3 weights are under the MiniMax H3 Community License, which limits territories. No weights are included.

The Studio that drives this library is #941; the CUDA context hook it can use is #946.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PfjmufXm4L7qsVNFyRw9ZK

@ashhart

ashhart commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Thank you, @drowzeys, the stored-bf16 H3 blocks provide the CUDA component to carry forward, with #947's contribution retained in the history.

We checked 96f0aa2. cuda.zig:84 forces device zero despite accepting host pointers; cuda_lib.zig:60-79 casts signed TP values before validating them. Geometry also grows unchecked byte products before its region check. We can repair context sharing with #946, validate the C ABI and budget shapes before allocation, with failure controls.

We will qualify single-device blocks first and TP separately, then finish native conditioning, projections, sampling and decoders with the shared H3 job/Metal design. Please retain identical-input per-stage fixtures and per-rank memory/failure cases; the current block library still leaves those surrounding pipeline stages to its host.

Posted by Ash's AI Assistant Synthena

@ashhart ashhart mentioned this pull request Oct 17, 2026
…for a host pipeline

A video model's denoising transformer: no lane round, and int8 products and attention on mma.sync m16n8k32. It is
a library (zig build tf-h3) beside the engine for a host that owns the rest of the pipeline and shares its CUDA
context; the lane core and the server are untouched. docs/recipes/minimax-h3-cuda.md holds the receipt.
A host that merges a LoRA into the checkpoint's weights hands the merged projection over and the library quantizes
it as at load; the int8 copy saved beside the checkpoint is left as it was.
A rank runs the rows of its own routing tiles; every block it hands the other ranks its tiles' int8 keys, values,
scales and pooled means and takes theirs, then attends as on one GPU. The kernels and their arithmetic are unchanged
row for row, so a pass on N ranks gives the bits of a pass on one.

- h3_xchg.c: the exchange over libibverbs. One registered region a rank, one reliable queue pair a peer and rail;
  a rank RDMA-writes its share into the same offsets of every peer's region, the last write carrying a sequence
  number. A rail is one RDMA device; two rails split each span.
- The region is pinned host memory; the buffers stay on the device and h3_spans packs a rank's tiles into the
  region and unpacks the others' out of it. Two halves of the region by block parity: a rank one block ahead never
  writes what another is unpacking.
- Inputs.tile0/tile1/cuts: a rank's tile range and every rank's. Routing, top-k and attention run for the rank's
  own query tiles; kernels take their tile range by where their buffers start.
- tf_h3_open on a .tf-int8 path opens the saved int8 copy alone (a rank holds no checkpoint).
- tf_h3_forward_host, tf_h3_host_alloc / _free: inputs in host memory, for a rank without a framework.
- tf_h3_tp_init / _close / _info / _connect / _stats. The connection records travel over the host's own channel.
- The sequence's buffers move to cuda_geometry.zig and the world to cuda_tp.zig, keeping each file under 600 lines.
…e recipe's receipt

contract_h3.zig registers h3_gemm, h3_gemm_n256, h3_gemm_swiglu and h3_gemm_wide (exact int32 sums, fp32 scales,
bf16 out) and h3_spans (a move) for gates 1 to 4, with their launches and per-SM records for gate 5. The H3 kernel
image is built once for the library and the contract's runner, and kernel-contract-fatbins installs it.
…Linear, a bf16 routed attention

The blocks now run at the checkpoint's own precision, and int8 is gone from the family.
- Weights: the checkpoint's bf16 tensors copied to the device as stored; no quantized copy.
- Products: zig/src/cuda/cublaslt.zig's Linear, bf16 in and out, fp32 compute, no TF32. Each shape keeps the
  algorithm chosen at 16,384 rows, so a rank's rows take the same sums as on one GPU.
- Attention: FastH3's tile-routed attention on mma.sync m16n8k16, fp32 scores and online softmax.
- Routing: tile means, scores, top-k and the pooled branch in fp32, as the Metal family (ashhart#949) does.
- Rows: norm, gated adds, SwiGLU and head layout round where torch's bf16 ops round in the reference.
- Ranks trade bf16 keys and values and fp32 key and value means.
Nine row, routing and attention kernels against their float64 formulas (ported from the Metal family's references),
the top-k against a CPU oracle under its stated NaN and tie rule, and h3_spans as before; 11 launch records.
…e a host's process

A library that runs kernels for a host framework (PyTorch, say) has to work in the host's context and on its
stream. Context.shared retains the primary context of the device the calling thread's current context is on
(device 0's when none is current) and refuses a context that is not a primary one; Stream.borrowed wraps a stream
the caller owns, null being the legacy default stream. Nothing that exists changes; cuCtxGetDevice joins the
driver table. tf-cuda-test shared-context checks both on a GPU.
@drowzeys
drowzeys force-pushed the upstream/h3-cuda-bf16 branch from 96f0aa2 to e1c121e Compare October 17, 2026 01:30
@drowzeys

Copy link
Copy Markdown
Contributor Author

Rebased on main 6ee828c2 together with #947; head 96f0aa2e is now e1c121e2. The pull request's diff against main is patch-identical to before (main's new commits only touch docs); zig fmt --check clean on the changed Zig files; python3 tools/zig/lean_check.py reports no new findings against main.

Model.load always retained device 0's primary context, though the library
runs inside a host process that may have made another device current. It now
takes Context.shared (ashhart#946): the current context's device, and device 0 only
when the host has none yet; a current context that is not its device's
primary context is refused. The legacy default stream is taken as
Stream.borrowed.
… them

tf_h3_tp_init, tf_h3_tp_info and tf_h3_tp_connect @intcast their c_int world,
rank and peer to u32 before anything looked at them, so a negative value
trapped (or wrapped in a release build). cuda_bounds.zig checks the signed
values first: 2 to 8 ranks, 0 <= rank < world, and a peer that is another rank
of the model's world, refusing with error.BadWorld. Its host tests run in
test-cuda-host.
… or allocated

geometry.prepare multiplied the host's u32 row and tile counts into byte
sizes unchecked, and only compared the exchange region after some buffers
were already made. cuda_bounds.bytes() now computes every buffer's bytes and
the region's with std.math.mul/add and refuses an overflow, or a row or slot
count past the kernels' int, with error.GeometryTooLarge (more prefix tiles
than tiles is error.BadGeometry). prepare budgets all of it, region included,
before it releases the previous geometry or allocates. Host tests: a real
sequence's sizes and each overflow.
@drowzeys

Copy link
Copy Markdown
Contributor Author

Thank you. All three points are fixed on the branch; the head is now 3dc1b767.

All of these are host tests, wired into test-cuda-host.

Checks on a DGX Spark (CPU only, no GPU work):

Check Result
zig fmt --check build.zig zig clean
lean_check 517, the same as main, no new findings
zig build test 88/88 steps
zig build test-cuda-host 43/43 steps
zig build tf-h3 -Dnvcc=… libtf_h3.so builds

Kernels and launches are unchanged, so I didn't rerun the kernel contract. I haven't rerun the library on a GPU since these commits. The identical-input per-stage fixtures and the per-rank memory and failure cases are all kept.

@ashhart

ashhart commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Thanks @drowzeys for the current-context, signed-value and checked-geometry repairs, including validation before replacing old geometry. The named refusal tests are useful host evidence; the changed library has not had its device rerun.

Our landing split is the BF16 single-device block library first, with shared runtime ownership and the later partial-load cleanup repairs retained. TP stays a separate follow-up with per-rank memory, exchange and failure gates. Please keep same-input stage references and allocation/refusal controls attached. This component still relies on external encoders and decoders, so it does not establish complete native clip serving.

Posted by Ash's AI Assistant Synthena

ashhart added a commit that referenced this pull request Oct 18, 2026
 beneath it

#958's transformer blocks, kernels and contracts as a C library for a host pipeline (zig build tf-h3, libtf_h3.so) on
one device; its sequence-parallel exchange over RoCE waits for a follow-up of its own. With our repairs: checkpoint and
projection bounds before any allocation, the host's primary context through Context.shared, complete blocks only,
partial loads keeping their destinations and parent objects until their copies are proven complete, and one recovery
walker for each failed-load registry. The H3 kernels join the CUDA kernel contract. The host steps (test-h3-ownership,
-metadata, -arithmetic, -blocks and -boundaries, compile-h3-library) sit beside the Metal family's in
zig/build/h3_host.zig, and the tests are named for what they check.
@ashhart

ashhart commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Thank you, @drowzeys, for H3/FastH3's CUDA transformer blocks. 1.1.0 includes #958 (070f09a3e) as the single-device bf16 libtf_h3.so library for a host generation pipeline. It uses runtime cuBLASLt with fp32 sums and the common copy owner. Checkpoint/projection bounds, complete block admission and partial-load cleanup retain buffers until copies complete. Sequence-parallel RoCE remains a follow-up. Numerical/resource and host-pipeline results: nvcc 13.0/13.2 builds and resource checks passed; real H3 checkpoint/host-pipeline cells remain unrun.

Posted by Ash's AI Assistant Synthena

BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
… with ashhart#947 beneath it

ashhart#958's transformer blocks, kernels and contracts as a C library for a host pipeline (zig build tf-h3, libtf_h3.so) on
one device; its sequence-parallel exchange over RoCE waits for a follow-up of its own. With our repairs: checkpoint and
projection bounds before any allocation, the host's primary context through Context.shared, complete blocks only,
partial loads keeping their destinations and parent objects until their copies are proven complete, and one recovery
walker for each failed-load registry. The H3 kernels join the CUDA kernel contract. The host steps (test-h3-ownership,
-metadata, -arithmetic, -blocks and -boundaries, compile-h3-library) sit beside the Metal family's in
zig/build/h3_host.zig, and the tests are named for what they check.
@ashhart

ashhart commented Oct 24, 2026

Copy link
Copy Markdown
Owner

Closing: this was merged into main as 070f09a ('Merge #958') for 1.1.0, and main's own validation has since superseded the later review commits. If you'd like the RoCE part of this branch considered, please open it as a new PR on current main. #1209 tracks RoCE beyond two ranks. Thank you @drowzeys.

- Synthema, Ash's AI Assistant.

@ashhart ashhart closed this Oct 24, 2026
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.

2 participants