## Summary Make Blackwell MoE homogeneous **1SM UMMA** so the native core builds with CUDA 13. The previous fused backward kernel combined 1SM token-gradient/activation phases with 2SM weight-gradient phases; CUDA 13 rejects mixed `.cta_group::1` and `.cta_group::2` instructions within one kernel. This PR contains **only the 1SM implementation**. Uniform-2SM alternatives were implemented and measured separately, including an NC=8 communication ablation, but are not included in the code change. Qwen is the priority workload: 1SM outperforms both tested 2SM alternatives in forward and backward for every measured Qwen shape. ### Implementation - Convert SM100 MLP3/MLP4 from joined M256xN256 two-CTA tiles to independent M128xN128 tiles, retaining the existing K tile and stage selections. - Use 1SM MMA atoms, CTA-local TMEM allocation, single-CTA TMA loads, local transaction-byte accounting, and one accumulator-release arrival per CTA. - Remove paired-CTA TMEM and accumulator rendezvous from fused MoE backward; retain the required CTA-local phase synchronization. - Keep communication geometry at **TileM=128, NC=4**. Forward math is unchanged. - Retain the existing tuning-table row identifiers for compatibility; map SM100 weight-gradient traits explicitly to the new tile geometry. - Update standalone harnesses and document CUDA 13 native-source support. The cross-entropy dW harness changes only its MoE reference path. **Unchanged:** production cross-entropy remains homogeneous 2SM; Hopper-specific production sources, release workflows, CUDA 12.9 release-toolchain pins, and published wheels are not changed. ## Performance methodology All tables report **TFLOP/s per GPU**, not the sum across GPUs. Measurements use eight NVIDIA B300 SXM6 PC GPUs, 8,192 input tokens per rank, eight microbatches, and 250 ms per-launch cooldown. Workload matching uses model, GPU count, T, D, I, E, and K, not just the model label. | Variant | Toolchain | MoE instruction groups | Communication TileM | NC | Weight-gradient tiles | |---|---|---|---:|---:|---| | Historical baseline | CUDA 12.9 | Original mixed 1SM/2SM backward | 128 | 4 | Joined M256xN256, 2SM | | **1SM: this PR** | CUDA 13.0.88 | Uniform 1SM | 128 | 4 | Independent M128xN128 | | 2SM, NC=4 ablation | CUDA 13.0.88 | Uniform 2SM in forward/backward | 256 | 4 | Joined M256xN256 | | 2SM, NC=8 ablation | CUDA 13.0.88 | Uniform 2SM in forward/backward | 256 | 8 | Joined M256xN256 | The CUDA 13 variants use the same B300 host/image, driver 580.159.04, PyTorch 2.11.0.1+cu130, and existing CUTLASS/NVSHMEM/TVM FFI installations. Their source base is `43293d84d760e402e4fd974b12410a1d47c31761`. The benchmark is MoEBenchmark at `85443d69ab065bfa4d303e1e4af56594c993ddf3`, with the same small adapter updates for all CUDA 13 variants: current team-handle arguments and the existing unique-ID bootstrap for single-node torchrun. Those changes are outside the timed kernel regions. Timing loops, max-rank CUDA-event latency, FLOP conventions, and correctness tolerances are unchanged. Forward FLOPs/GPU: `6*T*K*D*I + 5*T*K*I + 2*T*K*D`. Backward FLOPs/GPU: `16*T*K*D*I`. Percentages below are calculated from the CSV precision before display rounding. ### Workloads Here D is hidden size, I is per-expert intermediate size, E is total expert count, and K is routing top-k. T=8192 and world_size=8 for every row. | Model | D | I | E | K | |---|---:|---:|---:|---:| | Llama 4 Scout | 5120 | 8192 | 16 | 1 | | Mixtral 8x7B | 4096 | 14336 | 8 | 2 | | Mixtral 8x22B | 6144 | 16384 | 8 | 2 | | Qwen3 30B-A3B | 2048 | 768 | 128 | 8 | | Qwen3 235B-A22B | 4096 | 1536 | 128 | 8 | | Qwen3.5 35B-A3B | 2048 | 512 | 256 | 8 | | Qwen3.5 122B-A10B | 3072 | 1024 | 256 | 8 | ## Full comparison against the historical baseline Each ablation cell shows **TFLOP/s (throughput change vs. baseline)**. Positive is faster; negative is slower. ### Forward | Model | Baseline | 1SM: this PR | 2SM, NC=4 | 2SM, NC=8 | |---|---:|---:|---:|---:| | Llama 4 Scout | 886.7 | 851.6 (-3.96%) | 865.9 (-2.35%) | 920.8 (+3.85%) | | Mixtral 8x7B | 1,175.8 | 1,140.1 (-3.04%) | 1,203.1 (+2.32%) | 1,194.4 (+1.58%) | | Mixtral 8x22B | 1,174.3 | 1,155.9 (-1.56%) | 1,099.2 (-6.39%) | 1,107.6 (-5.68%) | | Qwen3 30B-A3B | 467.8 | 465.8 (-0.44%) | 366.8 (-21.60%) | 361.1 (-22.82%) | | Qwen3 235B-A22B | 762.2 | 766.4 (+0.56%) | 663.8 (-12.90%) | 704.3 (-7.60%) | | Qwen3.5 35B-A3B | 273.8 | 272.9 (-0.31%) | 232.5 (-15.09%) | 241.8 (-11.68%) | | Qwen3.5 122B-A10B | 551.1 | 554.9 (+0.68%) | 462.8 (-16.03%) | 489.7 (-11.14%) | ### Backward | Model | Baseline | 1SM: this PR | 2SM, NC=4 | 2SM, NC=8 | |---|---:|---:|---:|---:| | Llama 4 Scout | 714.0 | 614.6 (-13.93%) | 683.4 (-4.29%) | 731.5 (+2.45%) | | Mixtral 8x7B | 956.3 | 796.3 (-16.74%) | 916.4 (-4.18%) | 934.5 (-2.28%) | | Mixtral 8x22B | 1,018.2 | 839.1 (-17.59%) | 960.8 (-5.64%) | 989.1 (-2.86%) | | Qwen3 30B-A3B | 402.5 | 390.9 (-2.89%) | 327.7 (-18.58%) | 353.9 (-12.08%) | | Qwen3 235B-A22B | 646.2 | 577.8 (-10.58%) | 554.0 (-14.27%) | 571.6 (-11.53%) | | Qwen3.5 35B-A3B | 290.0 | 280.7 (-3.22%) | 226.1 (-22.05%) | 234.3 (-19.22%) | | Qwen3.5 122B-A10B | 534.0 | 489.3 (-8.36%) | 426.1 (-20.19%) | 436.4 (-18.27%) | ### Aggregate throughput changes These are unweighted geometric means of per-model throughput ratios, not arithmetic averages of TFLOP/s and not an end-to-end training-speed claim. | Comparison | Forward | Backward | |---|---:|---:| | 1SM vs. historical baseline | -1.17% | -10.64% | | 2SM NC=4 vs. historical baseline | -10.63% | -13.05% | | 2SM NC=8 vs. historical baseline | -8.02% | -9.44% | | 2SM NC=4 vs. 1SM | -9.57% | -2.69% | | 2SM NC=8 vs. 1SM | -6.94% | +1.34% | | 2SM NC=8 vs. 2SM NC=4 | +2.92% | +4.14% | ## Why choose 1SM? Qwen is the priority workload. Even the improved NC=8 2SM candidate is slower than 1SM for every measured Qwen shape in both directions: | Qwen model | 2SM NC=4 vs. 1SM forward | Backward | 2SM NC=8 vs. 1SM forward | Backward | |---|---:|---:|---:|---:| | Qwen3 30B-A3B | -21.26% | -16.15% | -22.48% | -9.46% | | Qwen3 235B-A22B | -13.39% | -4.13% | -8.11% | -1.07% | | Qwen3.5 35B-A3B | -14.83% | -19.46% | -11.41% | -16.54% | | Qwen3.5 122B-A10B | -16.59% | -12.91% | -11.75% | -10.81% | Uniform 2SM does have a useful counterexample: with NC=8, backward throughput exceeds 1SM by 19.03% for Scout, 17.36% for Mixtral 8x7B, and 17.88% for Mixtral 8x22B. Those gains explain its slightly positive all-model backward geometric mean, but do not justify choosing it for the Qwen-focused workload. The NC=8 experiment restores the communication band from 64 rows/CTA (`256/4`) to 32 (`256/8`, matching the original `128/4`). It improves backward over NC=4 for all seven shapes. However, it also changes the number of independent communication groups, ring capacity/adaptive grouping, and the existing NC-aligned launch grid: on this 148-SM GPU, NC=4 launches 148 CTAs/37 groups, while NC=8 launches 144 CTAs/18 groups. Other tuning parameters were not retuned. The proposed choice is therefore **uniform 1SM for CUDA 13 compatibility**, accepting the explicitly reported historical-baseline regressions rather than introducing the larger Qwen regressions and paired scheduling changes of the tested 2SM alternatives. Shape-selective 2SM tuning is outside this PR. ### Interpretation limits - The baseline is the recorded September CUDA 12.9 campaign, not a fresh same-image baseline. It differs in toolchain, dependencies, and intervening code. Its changes cannot be attributed exclusively to the UMMA instruction group. The unchanged-math 1SM forward control itself moves -1.17% geometrically. - The 1SM/2SM comparisons use the same CUDA 13 environment, but compare complete implementations: tile dimensions and required pairing/scheduling change with the instruction group. They do not isolate the causal cost of a single instruction choice. - These are single-campaign point estimates, without confidence intervals. In particular, small differences such as the +1.34% overall backward result are not statistical-significance claims. - The conclusion is scoped to these seven eight-GPU, 8K-token BF16 workloads; no claim is made about every Qwen configuration or multi-node performance. ## Validation - Full native CUDA 13 `sm_100f` core build. - B300 standalone MLP3/MLP4 correctness, MLP1 forward/activation-recompute correctness, and the separate MoE-reference/FSLCE-dW harness. - All seven eight-B300 model shapes compared with the existing Torch reference in forward and backward for the 1SM candidate and both 2SM ablations; no tolerances relaxed. - Retained Hopper MLP3/MLP4 harness correctness on H200 with CUDA 12.9. The benchmark's reference column is labeled "TE", but these runs were built with `HAVE_TE=OFF`: the reference is Torch matmul, not Transformer Engine. Reference comparisons were run separately from the eight-microbatch, cooled Liger-only performance measurements. ### Benchmark invocation Using the same existing MoEBenchmark executable and adapter for each candidate: ```bash export BENCH_NAMED_ONLY=1 BENCH_ONLY_CFG=_t8k export NVSHMEM_DISABLE_NCCL=1 NVSHMEM_REMOTE_TRANSPORT=none export NVSHMEM_SYMMETRIC_SIZE=6G OMP_NUM_THREADS=1 # Run separately for bench_moe_mlp and bench_moe_mlp_bwd. BENCH_BACKENDS=liger,torch_baseline N_MICROBATCHES=1 BENCH_COOLDOWN_MS=0 \ uv run --no-project python -m torch.distributed.run \ --standalone --nproc-per-node=8 --no-python ./build/bench_moe_mlp BENCH_ONLY_BACKEND=liger BENCH_SKIP_CORRECTNESS=1 \ N_MICROBATCHES=8 BENCH_COOLDOWN_MS=250 BENCH_CSV=forward_gpu8.csv \ uv run --no-project python -m torch.distributed.run \ --standalone --nproc-per-node=8 --no-python ./build/bench_moe_mlp ``` Native library SHA256 identifiers for the measured CUDA 13 candidates: ```text 1SM: 6226ae0c033515746eec2bf3bc2430624e10577f9b6ac2b72fd3ea45c90b162e 2SM NC4: 193decd95a38a8e2838379f84660c00ce42c2d3d4c027d167fae9d46c36265f8 2SM NC8: 8bc029baaecdab649a30199146867ef59e6da099801675fe49e7e50132f26b4b ``` Co-authored-by: jkolehma <[email protected]> Co-authored-by: Copilot <[email protected]>
43 KiB
LigerCute — native CUTLASS + NVSHMEM kernels
Overview
liger_cute_kernels is the native CUTLASS + NVSHMEM kernel package for Liger.
It currently provides expert-parallel MoE and tensor-parallel fused scaled
linear cross entropy, with a Torch-ABI-independent core exposed through TVM FFI.
LigerMoE provides persistent expert-parallel forward and backward kernels for NVIDIA Hopper and Blackwell GPUs. Each participating CTA uses warp specialization so communication and tensor-core computation make progress on the same SM: dedicated warps move routed token tiles while the MMA warps execute the expert MLP. One-sided NVSHMEM RDMA removes sender/receiver coordination, and preallocated symmetric buffers are sized from the total routed-token capacity rather than a fixed per-expert capacity. This keeps the execution compatible with CUDA Graphs despite dynamic routing.
The tensor-parallel fused scaled linear cross-entropy implementation fuses the classifier projection with per-token NLL and optional entropy. Local MAX/SUM reductions run in the tensor-core epilogue through NVLS or DirectPeer, with a sharded inter-host ring follow-up for multi-host execution.
Public runtime configuration
Use liger_cute_kernels.configure(...) to initialize NVSHMEM once and prepare
FLSCE and/or MoE through one public setup boundary. This API requires the LCK
Python package containing this extension; older wheels do not expose it.
Use a matching native build exposing the TP and EP context entry points.
Successful native setup returns None. Unsupported devices raise
liger_cute_kernels.UnsupportedDeviceError without loading or initializing
native resources. The liger_kernel.ops.configure.configure frontend catches
that specific error, emits a warning, and returns False. Consumers should
use that Liger-Kernel frontend rather than inspect device capabilities or
call LCK directly; operator frontends continue to own fallback selection.
import torch
import torch.distributed as dist
from liger_cute_kernels import FusedLinearCrossEntropyConfig, MoEConfig, configure
# Initialize torch.distributed and create tp_group/ep_group first.
torch.cuda.set_device(local_rank)
configure(
process_groups={"tp": tp_group, "ep": ep_group},
bootstrap_group=dist.group.WORLD,
device=torch.device("cuda", local_rank),
flsce=FusedLinearCrossEntropyConfig(
max_tokens=8192,
hidden_size=4096,
local_vocab_size=32000,
group="tp",
),
moe=MoEConfig(
max_tokens=8192,
hidden_size=4096,
num_experts=128,
top_k=8,
num_hosts=1, # This example assumes each EP group is within one host.
gpus_per_host=dist.get_world_size(ep_group),
group="ep",
max_inflight=1, # Increase for multiple MoE forwards awaiting backward.
),
)
The bootstrap group defines NVSHMEM_TEAM_WORLD; it is not inferred from
the first operator's TP or EP group. TP and EP can use different partitions
of this common domain. process_groups accepts a Mapping[str, ProcessGroup];
names are arbitrary, and operator sections select them with group="name".
group=None selects the bootstrap group. Unknown names and None mapping
values are rejected rather than silently selecting WORLD.
Every declared group is prepared in sorted-name order, even if it is not yet used by an operator. Additional names can be registered in later calls; omitted names remain available until finalization. Reusing a name requires unchanged ordered membership. Merely registering more groups does not create additional operator workspaces.
Every bootstrap rank must call configure, in the same
order, with the same operator sections, capacities, and topology. Local operator
groups must form consistent, equal-sized, arithmetic-stride partitions in
bootstrap PE numbering. All ranks must use matching GPU architectures and SM
counts; TP groups must also have matching host topology. Multi-host TP
membership must be uniform and host-major. MoE's num_hosts and
gpus_per_host describe each EP team, not the full bootstrap domain.
Choose the bootstrap domain to include all intended TP and EP members. With
pipeline parallelism, either arrange coordinated setup on every bootstrap rank,
including ranks without an output head, or explicitly choose a smaller domain
whose members all participate. Do not call a WORLD-collective setup only on
post_process chunks.
Calling configure without operator sections initializes only the runtime.
A later collective call can add an operator without clearing the other's
resources. Repeating an already prepared section with equal or smaller capacity
reuses its native reservation. Reserve the maximum across actor/reference consumers
upfront, including fixed-size microbatches; requests exceeding the reservation
raise before kernel launch. FLSCE hidden capacity includes the frontend's
alignment padding. MoE token, hidden, expert-count, and top-k requests may be
smaller than the reserved limits.
FLSCE capacities remain process-wide, while native execution contexts are cached by NVSHMEM team handle. Each context has its own local/remote teams, peer mappings, signals, and launch epochs. Bulk scratch is shared through the existing buffer pool, sized once for the maximum problem across supported TP topologies. Prepare additional actor/reference TP groups with another collective call; the groups can have different TP sizes:
configure(
process_groups={"reference_tp": reference_tp_group},
flsce=FusedLinearCrossEntropyConfig(
max_tokens=2048, # Must fit the original process-wide reservation.
hidden_size=4096,
local_vocab_size=local_vocab_size,
group="reference_tp",
),
)
Pass the appropriate process group to each FLSCE call. Backward retains the
forward's team, and captured graphs retain their original context. Equivalent
process-group objects with the same ordered membership reuse a native context.
The Python fast path caches ProcessGroup -> team_handle, so repeated execution
does not reconstruct rank lists or create teams. Native context lookup does not
allocate or query topology.
Additional contexts reserve only their signaling and mapping state, not another copy of the bulk scratch buffers. Symmetric signal allocations use collectively ordered slots, not local team-handle values. When only some teams in a new partition are already cached, every bootstrap PE still reserves that slot to preserve NVSHMEM heap ordering; cached teams continue using their original signals and epochs. Slot storage is retained until pool clear or finalization.
The pool contains reusable internal buffers. Forward's saved LSE/entropy are ordinary PyTorch tensors, not pooled or symmetric-stack allocations. Symmetric tensors that must remain live across calls continue to use the symmetric stack; this change does not alter its ownership rules.
MoE uses the same group lifecycle: repeated configure(moe=MoEConfig(...))
calls prepare additional EP partitions beneath the original process-wide
capacities, including different EP sizes. MoE visits peers in team-local
round-robin order, (rank + step) % team_size, starting with the local PE.
It needs neither device-global schedule tables nor host-topology fields in
the kernel iterators, and does not assume a particular HCA layout.
Each EP context owns its device workspace and symmetric-stack names. Fixed-size
symmetric payload scratch stays shared. Pass the forward's team to low-level
moe_pop_fwd(team_handle); the autograd wrapper does this automatically.
MoEConfig.max_inflight (default 1) reserves the maximum number of forwards
awaiting backward per EP context. Count all MoE layers and microbatches whose
intermediates can be live together. Forward intermediates remain in the symmetric
stack, not the buffer pool. Reservation occurs collectively in configure, so
independent EP execution never triggers a WORLD allocation. Exceeding this
capacity raises before launch. Pop forwards in LIFO order within each context;
different contexts have independent stacks. Equivalent groups share a context
and its retained-forward limit. Warm up the shapes/configurations to be captured
before capturing their graphs; a group's larger device scratch never invalidates
another group's captured pointers.
This API supports one CUDA device per process. Growing
a live reservation remains unsupported. Call
setup serially at a coordinated boundary, outside CUDA graph capture. Serialize
all native launches and graph replays on each process, including across different
TP/EP groups: use the same CUDA stream or explicit stream synchronization. Issue
collectives in consistent order across participating ranks. Destroy teams or clear pools only
after all dependent launches, backwards, and graphs have finished.
The legacy low-level MoE setup retains WORLD-collective lazy stack allocation;
use public configure for independent subgroup execution.
Non-communicating kernels need no group workspace. The Triton vocab-parallel CE path already uses the caller's process group and per-call tensors; it does not share NVSHMEM counters or require a native capacity section.
One API through either package
Downstream integrations can use the same API through Liger without importing
NVSHMEM or TVM FFI. The public liger_kernel.ops.configure module contains a
thin entry point and dependency-free configuration data classes, not per-op
setup methods. It converts the data to LCK's configuration objects only when
LCK is installed. For Megatron, pass
the relevant initialized entries of its pg_collection explicitly; LCK does
not depend on Megatron's collection type:
from liger_kernel.ops.configure import FusedLinearCrossEntropyConfig, MoEConfig, configure
native_configured = configure(
process_groups={"tp": pg_collection.tp, "ep": pg_collection.ep},
bootstrap_group=dist.group.WORLD,
device=torch.device("cuda", local_rank),
flsce=FusedLinearCrossEntropyConfig(
max_tokens=8192,
hidden_size=4096,
local_vocab_size=32000,
group="tp",
),
moe=MoEConfig(
max_tokens=8192,
hidden_size=4096,
num_experts=128,
top_k=8,
num_hosts=1,
gpus_per_host=dist.get_world_size(pg_collection.ep),
group="ep",
),
)
There is one signature: configure(*, process_groups=None, bootstrap_group=None, device=None, flsce=None, moe=None). Operator-specific values belong to configuration data,
not methods on the operator classes. Either section can be omitted. The
liger_kernel.ops.cute implementation namespace remains private and does not
export this configuration API.
Ordinary operator imports do not expose or load this optional setup API.
Importing liger_kernel.ops.configure, importing its function or classes, and
constructing configuration objects all work without LCK installed. LCK is
loaded only when configure is called. If LCK is absent or does not support
the device, the function emits a
warning and returns False without changing the runtime; other Liger kernels
remain usable. This warning conversion belongs to Liger-Kernel: LCK itself
raises UnsupportedDeviceError. Successful setup through the Liger-Kernel
frontend returns True. Other failures inside an
installed LCK package still propagate, including invalid capacity, inconsistent
collective plans, and conflicting NVSHMEM ownership. All bootstrap ranks must
use a consistent installation and matching hardware. Skipping setup does not
enable an explicitly selected native-only
operator or change backend selection.
The operations' existing .apply(...) interfaces and fallback dispatch remain
unchanged.
Ownership, offload, and teardown
Set NVSHMEM environment variables before the first configuration call. Configuration checks the visible host runtime's initialization status and rejects already initialized unmanaged runtimes, including DeepEP V1 RDMA. Sharing ownership is unsupported in either initialization order: do not initialize another NVSHMEM owner after LCK. This check is not a general interoperability mechanism for separately loaded or hidden runtimes. DeepEP V2 and V1 paths that do not initialize NVSHMEM are not rejected merely because their Python modules are imported.
The symmetric heap and workspaces remain resident during parameter/optimizer
offload and colocated inference-engine sleep/wake. Account for them in the
device memory budget; model offload does not release them.
There is no per-model automatic finalization. Once all forwards, backwards,
and graph replays have finished, synchronize and collectively call
liger_cute_kernels.nvshmem.finalize() before destroying torch process groups.
For an explicit quiescent pool reset, nvshmem.pool_clear_all() invalidates
both operator reservations; configure them again before use. Partial
pool_clear_buffers() is rejected while the public API manages resources.
LigerMoE design
Forward pass
The forward path sorts routed tokens and copies them to symmetric memory before entering the persistent main loop. Within that loop, get, TMA, MMA, and put warps pipeline remote reads, tensor-core work, and result delivery. Intra-host traffic uses direct TMA access over NVLink or PCIe; inter-host traffic uses NVSHMEM get/put operations. A final local combine applies the router weights after every expert result has arrived.
Backward pass
The backward path first produces routed expert-output gradients, then overlaps
remote X/dY movement with activation recomputation, input-gradient
calculation, and expert-weight gradients. TMA reduce stores accumulate weight
gradients across token-tile batches. The final local dispatch reduces the
routed dX contributions back to the original tokens.
MoE performance
The current evaluation uses BF16 and the CUDA 12.9 environment. These are benchmark snapshots rather than performance guarantees for every model or system.
| Evaluation | Current result |
|---|---|
| Qwen3-30B-A3B, 8 H200 GPUs, 8,192 tokens/rank | 233 TFLOP/s forward and 209 TFLOP/s backward, respectively 49% and 31% above the strongest plotted baselines |
| Qwen3-30B-A3B, 8 B300 GPUs, 8,192 tokens/rank | 468 TFLOP/s forward and 402 TFLOP/s backward |
| Qwen3-30B-A3B training, 8 H200 GPUs | 44.936 s mean step time, a 1.472x speedup over the Megatron baseline |
| Qwen3.5-122B-A10B training, 16 B200 GPUs | 126.489 s mean step time, a 1.070x speedup over the Megatron baseline |
Throughput across expert-parallel GPU counts
H200 throughput at 8,192 tokens per rank from 1 to 16 expert-parallel GPUs.
Top: Qwen3-30B-A3B (D=2048, I=768). Bottom: Llama-4-Scout (D=5120,
I=8192). Left: forward. Right: backward. DeepEP and Comet require multiple
GPUs, and FlashMoE is forward-only.
B300 throughput for the same model shapes and token count. Comet and FlashMoE are omitted because they do not support Blackwell. The 16-GPU points span two hosts.
Throughput across token counts
H200 throughput from 1,024 to 16,384 tokens per rank on 8 GPUs. Longer token sequences deepen the transport/compute pipeline and improve SM utilization.
B300 throughput over the same token range on 8 GPUs. The panels use
Qwen3-30B-A3B (E=128, K=8) and Llama-4-Scout (E=16, K=1).
End-to-end training
OpenWebText pre-training with sequence length 8,192 for 300 steps. Panels (a)
and (b) show Qwen3-30B-A3B on 8 H200 GPUs (EP=8, global batch size 256);
panels (c) and (d) show Qwen3.5-122B-A10B on 16 B200 GPUs across two hosts
(EP=16, global batch size 512). Mean step-time statistics exclude the first
ten CUDA-Graph warm-up steps. The three backends retain effectively equivalent
loss convergence in both experiments.
Fused linear scaled cross entropy
The native cores also provide
fused_linear_scaled_cross_entropy_forward and
fused_linear_scaled_cross_entropy_backward. The backward path uses a
three-stage cluster-2 dZ handoff followed by a four-stage combined dX+dW
cluster kernel. dX uses split-K=2 only for hidden size 2,048; hidden size 4,096
uses split-K=1. Direct-peer and hierarchical multi-host transports remain
available when the single-host NVLS path is not selected.
The liger_cute_kernels.tvm_ffi facade exposes configuration plus forward and
backward entry points. Both accept the NVSHMEM team configured for the same
tensor-parallel ranks and take inverse temperature.
Capacities for tokens, hidden size, local vocabulary, and reduction grouping
must be configured before capture or execution.
Hierarchical NVLS+remote execution supports uniformly partitioned multi-host
subgroups. Team ranks must be host-major: each host contributes the same
number of consecutive team-local ranks. Setup applies
nvshmem_team_split_2d to derive an NVLS-capable local row and a matching-rank
remote column. For example, world ranks {0,4,8,12} on two eight-GPU hosts
become local teams {0,4} / {8,12} and remote pairs {0,8} / {4,12}.
Tensor-parallel reduction transport is implemented in liger_cute::detail.
Host setup selects either the NVLS or DirectPeer local backend and passes only
that backend's compact device view to the kernel. Multi-host setup retains the
parent-relative local and remote teams until configuration reset. The ring
topology, epochs, packed online-softmax state merge, and node-local all-gather
are shared by SM90 and SM100. The inter-host ring is a warp-scoped
device function: one worker warp avoids block synchronization for fusion,
while standalone wrappers use eight worker warps. SM100 forward partitions
the local vocabulary into configurable N256 waves (64 tiles by default),
keeps four source slots, and runs
the matching-rank host ring plus final state accumulation in warp 1 of one
stable CTA while later waves continue through TMA, UMMA, and the epilogue.
After the last wave, warp 1 performs the node-local NVLS all-gather and writes
NLL/LSE/entropy. SM90 forward retains the separately launched finalizer.
Backward uses the same ring between its local reduce-scatter and local
all-gather/scatter stages.
SM100 remote kernels synchronize on the configured parent TP team, then use a
cooperative clustered CUDA launch. They do not require the TP team to equal
NVSHMEM_TEAM_WORLD, so disjoint TP groups can execute independently.
On SM100 the backward is a single persistent 384-thread, cluster-2 kernel with
a device-side token-wave loop: dZ, dX, dW and the wave schedule are fused into
one launch. Warp 2 owns every TMA producer, warp 3 every UMMA issue, and warps
4-11 the epilogues and TMEM loads. Warp 4 takes a single Allocator2Sm TMEM
allocation, sized by the max over phases, that dZ, dX and dW reuse; the three
operand arenas form a phase-serial union while pipelines, mbarriers and the
TMEM handle live outside it. dW wave 0 stores and later waves TMA-reduce-add,
exactly like SM90. Every compute-side named barrier excludes warps 0 and 1, and
a full-grid software barrier is taken only where dZ workspace publication and
reuse require it, under a strict full-residency launch invariant.
The dX communication runs a three-stage chunk pipeline. A chunk is one token wave's complete dX: every CTA of the grid contributes a disjoint set of M128xN256 tiles into one packed shard region, so a chunk is exactly one contiguous inter-host message.
- Stage R — tile granular. Warp 0 NVLS reduce-scatters each tile the dX epilogue publishes and releases its staging slot immediately; it never waits for the rest of the chunk.
- Stage P — chunk granular. After its last tile of chunk
k, every CTA's warp 0 bumps a monotone completion counter. CTA 0's warp 1 launches the inter-host transfer only once the whole chunk is locally reduced. When the peer shard arrives, warp 1 from every resident CTA merges a global-strided section into the packed shard and contributes one HBM atomic arrival. CTA 0 acknowledges the ring and publishes completion after allgrid_ctasarrivals. - Stage F — chunk granular, deferred by one chunk. Warp 0 finalizes chunk
k(node-local NVLS all-gather plus the BF16 scatter intograd_input) at the top of chunkk+1, so it overlaps chunkk+1's dZ GEMM instead of gating it. The last chunk is drained after the wave loop.
The communication pipeline therefore spans both the same chunk's dW GEMM and the next chunk's dZ GEMM:
compute | dX(k) | dW(k) | dZ(k+1) | dX(k+1) | dW(k+1)
warp 0 | R(k) tile by tile | F(k) | R(k+1) ...
warp 1s | | IB transfer + all-CTA warp-1 merge | IB dX(k+1) ...>
dX(k) is finalized before its slots could be reused, not before compute
advances: the end-of-wave grid barrier protects the dZ workspace only, the
tile-granular staging ring is released in stage R, and the packed shard plus
the all-gather destination are addressed by absolute chunk index, so the two
live chunks always own disjoint storage. SM90 backward retains its two
separately launched wave kernels and its standalone finalizer.
The wave width can be selected at build time with
-DLIGER_CUTE_FSLCE_SM100_WAVE_N_TILES=16|32|64|128.
The ring publishes each whole shard and its ready epoch with one blocking
nvshmemx_qp_float_put_signal_warp on NVSHMEMX_QP_DEFAULT; it contains no
nvshmem_quiet, nvshmem_fence, or put_block calls. Two-host execution
alternates ready/inbox slots by wave sequence and needs no consumed signals.
Larger host rings retain consumed acknowledgements and pipeline their final
waits across waves by default.
On B300 two-host runs, pin one PE to each GPU-local HCA:
export NVSHMEM_REMOTE_TRANSPORT=ibrc
export NVSHMEM_IB_ENABLE_IBGDA=1
export NVSHMEM_ENABLE_NIC_PE_MAPPING=1
unset NVSHMEM_HCA_LIST
export NVSHMEM_HCA_PE_MAPPING='mlx5_0:1:1,mlx5_2:1:1,mlx5_3:1:1,mlx5_4:1:1,mlx5_5:1:1,mlx5_6:1:1,mlx5_8:1:1,mlx5_9:1:1'
With NVSHMEM_DEBUG=INFO, initialization should report
Successfully initialized the transport: IBGDA. It will be used for device-side APIs over IB.
For transport attribution, the SM100 remote stage can instead use matching-rank
warp MAX/SUM collectives with
-DLIGER_CUTE_FSLCE_SM100_USE_WARP_TEAM_COLLECTIVES=ON. This two-pass mode is
an ablation; the QP combined put-signal ring remains the default.
The distributed forward benchmark keeps the production Verl-derived fallback selectable and reports full-forward latency, effective global TFLOP/s, and maximum per-rank CUDA memory:
torchrun --standalone --nproc_per_node=8 \
benchmark/scripts/benchmark_fused_scaled_linear_cross_entropy_tp.py \
--provider native
torchrun --standalone --nproc_per_node=8 \
benchmark/scripts/benchmark_fused_scaled_linear_cross_entropy_tp.py \
--provider verl-fallback
Package architecture
The release wheel packages one core for Hopper and one for the common Blackwell-family ISA. The Python facade selects the matching core for the active GPU:
| Artifact | Sources | Links | Boundary | Built |
|---|---|---|---|---|
libliger_cute_kernels_sm90a.so |
csrc/core + liger_cute_kernels/tvm_ffi_bindings.cpp |
CUTLASS + NVSHMEM + CUDA + TVM FFI — no torch | flat extern "C" (liger_cute.h) and TVM FFI exports (__tvm_ffi_*) |
once per release |
libliger_cute_kernels_sm100f.so |
same | same | same | once per release |
The core's public ABI is extern "C" only (no std::/torch types cross it),
symbols are hidden except liger_cute_* and __tvm_ffi_*, and libstdc++/libgcc
are linked statically. That makes the core ABI-agnostic: the Python binding
is TVM FFI/DLPack based and is not tied to a specific torch wheel or runtime JIT
compile.
Two separate wheels
The top-level liger_kernel wheel is pure Python/Triton and does not
build or contain any of this native code. The native libraries ship as a
separate liger-cute-kernels distribution with the same public version.
It installs its own standalone top-level package liger_cute_kernels.
liger_kernel.ops.cute imports
liger_cute_kernels.tvm_ffi at runtime. Intended order:
- Install the top-level
liger_kernelwheel (pure Python). - Optionally install the matching native wheel (package
liger_cute_kernels) for the local CUDA + torch environment.
The native wheel is built by this module's setup.py (see Building the
native wheel below). Selecting/installing the right wheel automatically for
the local CUDA + torch environment is a separate follow-up.
Layout
liger_cute_kernels/ is a standalone module at the repo root holding
everything needed to build the native libraries. Only __init__.py (the
in-liger entry point) lives separately, under src/liger_kernel/ops/cute/.
liger_cute_kernels/ # ← standalone native build module (repo root)
├── README.md
├── assets/ # README benchmark figures
├── setup.py # builds the native wheel (package liger_cute_kernels)
├── pyproject.toml
├── cute_build.py # build_core() helper + native wheel build extension
├── liger_cute_kernels/ # native package source
│ ├── __init__.py # (.so are added here at build time)
│ ├── tvm_ffi.py # Python facade over the TVM FFI exports
│ └── tvm_ffi_bindings.cpp # TVM FFI C++ exports compiled into the core
├── test/ # native package unit tests
│ └── test_moe_bindings.py
├── CMakeLists.txt # core with TVM FFI exports
├── cmake/
│ ├── FindNVSHMEM.cmake
│ └── FindCUTLASS.cmake # locates main + tools/util/include
└── csrc/
├── core/ # → libliger_cute_kernels.so (torch-free)
│ ├── include/liger_cute/
│ │ ├── {liger_cute.h, export.h} # flat extern "C" ABI (C-parseable)
│ │ ├── {check.h, moe.h} # core control/config surface
│ │ └── detail/symmetric_memory.h # core-internal (nvshmem+STL); not ABI
│ ├── src/ # *.{cu,cpp} compiled INTO the core …
│ │ ├── moe/ # expert-parallel MoE kernels
│ │ │ └── tune/ # standalone offline autotuner — NOT a core source
│ │ └── fused_scaled_linear_cross_entropy/
│ │ # tensor-parallel fused loss kernels
│ └── liger_cute.version # exports only liger_cute_* and __tvm_ffi_*
src/liger_kernel/ops/cute/
└── __init__.py # runtime entry point: liger_kernel.ops.cute
# (loads liger_cute_kernels.tvm_ffi if installed)
Prerequisites
-
CUDA toolkit 12.9 or 13.0 with
nvcc(release builds pin 12.9.1) and either SM 9.0a (Hopper /sm_90a) or Blackwell family (sm_100f) support. The family target covers both B200 (sm_100) and B300 (sm_103) while enabling TCGEN05 UMMA and TMEM. CUDA 13.0 is also supported for native source builds. Blackwell MoE uses only.cta_group::1instructions: MLP3/MLP4 use M128xN128 tiles, CTA-local TMEM and single-CTA TMA loads, matching the other fused backward phases. The separate Blackwell cross-entropy kernels retain.cta_group::2. This avoids mixing instruction groups within a kernel, which CUDA 13 rejects. Release wheels remain on CUDA 12.9; this does not change the release toolchain or previously published artifacts. -
NVSHMEM install (host
.so, device.a, headers). Two layouts are supported:- Native/system install: point
NVSHMEM_HOMEat it, or use the default/usr/local/nvshmem. - PyPI install: install
nvidia-nvshmem-cu12==3.6.5for CUDA 12.9 ornvidia-nvshmem-cu13==3.6.5for CUDA 13. The Python wheel builder andbuild_core()auto-detect that package layout and create unversioned compatibility symlinks for CMake when needed. For direct CMake invocation, pass the package root as-DNVSHMEM_HOME=...; the find module accepts its versionedlibnvshmem_host.so.3.
- Native/system install: point
-
CUTLASS headers (4.x) — point
CUTLASS_HOMEat the repo root (so that$CUTLASS_HOME/include/cutlass/cutlass.hand$CUTLASS_HOME/tools/util/includeexist). Not needed when linking a prebuilt core. -
CMake ≥ 3.24; Ninja recommended.
-
apache-tvm-ffi at build and runtime. CMake uses
tvm-ffi-configto compile the TVM FFI exports, andliger_cute_kernels/tvm_ffi.pyuses the Pythontvm_ffiloader at runtime:python -m pip install apache-tvm-ffi
Build commands below are run from the repository root; the CMake project is
the liger_cute_kernels/ module directory.
Building
1. Core only (torch-free)
No torch required. This is the expensive CUTLASS compile, done once.
cmake -S liger_cute_kernels -B build/core \
-DLIGER_CUTE_BUILD_BINDINGS=OFF \
-DCMAKE_BUILD_TYPE=Release -GNinja
cmake --build build/core --target liger_cute_kernels -j
# -> build/core/csrc/core/libliger_cute_kernels.so
To also build the opt-in whole-program SM90 MoE module:
cmake -S liger_cute_kernels -B build/core \
-DLIGER_CUTE_BUILD_BINDINGS=OFF \
-DLIGER_CUTE_ENABLE_SM90_NONRDC_MOE=ON \
-DCMAKE_BUILD_TYPE=Release -GNinja
cmake --build build/core --target liger_cute_kernels -j
One cubin is placed beside libliger_cute_kernels.so. It contains separate
compile-time local and IB-capable kernel instantiations. At runtime, set
LIGER_MOE_SM90_NONRDC=1 before moe_configure_symmetric. The core loads the
module into the current CUDA context, registers it with
nvshmemx_cumodule_init, and uses it for SM90 forward and backward launches.
Each launch translates the configured EP team's members into
NVSHMEMX_TEAM_NODE on the host. If every member belongs to the node team, the
local specialization is selected; otherwise the IB-capable specialization is
selected. The local specialization compiles out IB transport and device-side
nvshmem_ptr probing.
The initial module contains the tuned Mixtral-8x7B T=8192 forward and
backward specializations; other shapes continue through the ordinary RDC
kernels. Add -DLIGER_CUTE_SM90_NONRDC_ALL_CONFIGS=ON for the complete tuned
menu and 1/2/4/8/16-PE benchmark coverage, including the cross-host IB path.
For development artifacts in a different directory, set
LIGER_MOE_SM90_NONRDC_CUBIN.
Build for Blackwell by overriding the CUDA architecture:
cmake -S liger_cute_kernels -B build/core-sm100f \
-DLIGER_CUTE_BUILD_BINDINGS=OFF \
-DLIGER_CUTE_CUDA_ARCH=100f \
-DCMAKE_BUILD_TYPE=Release -GNinja
cmake --build build/core-sm100f --target liger_cute_kernels -j
Use architecture-specific 100a or 103a targets only when a kernel requires
features outside the common Blackwell-family ISA.
Or from Python (with liger_cute_kernels/ on sys.path):
from cute_build import build_core
build_core("build/core") # stages one core and an optional SM90 cubin
build_core() auto-detects both native and PyPI NVSHMEM installations. Direct
CMake builds against the PyPI package must pass its root explicitly:
NVSHMEM_PYPI_HOME="$(python -c \
'import importlib.util; s=importlib.util.find_spec("nvidia.nvshmem"); print(next(iter(s.submodule_search_locations)))')"
cmake -S liger_cute_kernels -B build/core \
-DNVSHMEM_HOME="${NVSHMEM_PYPI_HOME}" \
-DLIGER_CUTE_BUILD_BINDINGS=OFF
2. Core + TVM FFI exports from source (single local build)
cmake -S liger_cute_kernels -B build/all \
-DLIGER_CUTE_BUILD_BINDINGS=OFF \
-DCMAKE_BUILD_TYPE=Release -GNinja
cmake --build build/all --target liger_cute_kernels -j
3. Wheel build against a prebuilt core (reuse across builds)
Reuses an existing libliger_cute_kernels.so instead of recompiling the core.
CUTLASS is not even searched here. This mode is useful when packaging a core
that was already compiled in a separate step.
cmake -S liger_cute_kernels -B build/bind \
-DLIGER_CUTE_BUILD_BINDINGS=OFF \
-DLIGER_CUTE_CORE_IMPORTED_DIR=/abs/path/to/dir-with-core \
-DCMAKE_BUILD_TYPE=Release -GNinja
cmake --build build/bind --target liger_cute_kernels -j
4. Offline autotuner (tune_moe_fwd_bwd) — optional, not in the wheel
csrc/core/src/moe/tune/ holds a standalone executable that regenerates the
tuned-config tables (csrc/core/src/moe/moe_fwd_bwd_tuning_configs_{single,multi}.cuh)
the runtime auto-dispatcher searches. It is not built by the core/bindings
targets above or shipped in the wheel: it has its own CMake project, and it
links the templated kernel launchers that the core .so deliberately hides
(visibility hidden + version script), so it recompiles moe.cu / moe_bwd.cu
itself at default visibility against torch. That makes it a full CuTe compile
(~45 min) — build it on demand, only when retuning.
Needs CUTLASS_HOME, NVSHMEM_HOME, and an importable torch (same as the
bindings). Build it as its own project (note -S points at the tune/ dir, not
the repo root):
cmake -S liger_cute_kernels/csrc/core/src/moe/tune -B build/tuner \
-DCMAKE_BUILD_TYPE=Release -GNinja
cmake --build build/tuner -j
# -> build/tuner/tune_moe_fwd_bwd
Run one rank per GPU under a PMI bootstrap; point the output env var at the table
you are regenerating (without it the .cuh is written to the current directory):
LIGER_MOE_FWDBWD_TUNED_OUTPUT=/abs/path/to/moe_fwd_bwd_tuning_configs_multi_sm90.cuh \
srun --mpi=pmi2 --ntasks=8 ./build/tuner/tune_moe_fwd_bwd # multi-PE class
# single-PE class (all experts local): --ntasks=1 + the _single_sm90.cuh output path
Cross-rank diagnostics can deliberately vary otherwise independent rank-local
work: MOE_FWDBWD_TUNE_TOKEN_SKEW=N subtracts rank*N tokens,
MOE_FWDBWD_TUNE_HETEROGENEOUS_SEED=1 cycles forward templates by rank, and
MOE_FWDBWD_TUNE_DETERMINISTIC_SEED=1 forces the first candidate instead of
each rank's measured winner. Combine them with MOE_FWDBWD_TUNE_VERBOSE=1 to
log the selected templates. MOE_FWDBWD_TUNE_COOLDOWN_MS=N sleeps outside
timed CUDA events, and MOE_FWDBWD_TUNE_REVERSE_CANDIDATES=1 reverses sweep
order to expose thermal/order bias. MOE_FWDBWD_TUNE_FWD_ONLY=1 skips the
backward sweep for forward-only diagnostics and therefore does not produce a
complete tuned row. MOE_FWDBWD_TUNE_T, _D, _I, _E, and _FAMILY can
pin an exact named-shape run; MOE_FWDBWD_TUNE_K=N overrides generic-sweep
top-k. Routed inputs are deterministic by default;
MOE_FWDBWD_TUNE_INPUT_SEED=N selects a different reproducible input and
router distribution. MOE_FWDBWD_TUNE_EXACT=1 runs exactly one arbitrary
shape and requires the T, D, I, and local-E overrides (plus optional
K).
Building the native wheel
This module's setup.py packages the native libraries into the independent
liger-cute-kernels distribution. The release wheel contains both
libliger_cute_kernels_sm90a.so and
libliger_cute_kernels_sm100f.so; NVSHMEM is installed independently through
the optional cu12 or cu13 extra, pinned to version 3.6.5.
The base package does not install either NVSHMEM distribution. The release workflow builds
both cores with CUDA Toolkit 12.9.1 in a digest-pinned
nvidia/cuda:12.9.1-devel-ubuntu22.04 container.
The Linux x86-64/glibc 2.35 wheel baseline and GPU targets are unchanged.
Release-wheel consumers need a CUDA 12-compatible runtime and NVIDIA driver. Do not mix
CUDA 12 cores with the CUDA 13 NVSHMEM package or vice versa; both distributions install
into nvidia/nvshmem, so use a clean environment rather than installing
nvidia-nvshmem-cu12 and nvidia-nvshmem-cu13 together. Previously published
CUDA 12.9 wheels are not changed by this build update.
Choose the NVSHMEM dependency explicitly when installing a built wheel:
python -m pip install "liger-cute-kernels[cu12]"
# For a local Hopper wheel built with CUDA 13:
python -m pip install "/path/to/liger_cute_kernels-<version>-py3-none-linux_x86_64.whl[cu13]"
Extras select dependencies only; they neither choose the CUDA compiler nor
convert the packaged binaries to another CUDA version. Do not request
[cu12,cu13]: both packages write to the same namespace. Existing system-managed
NVSHMEM users can install the base wheel without either extra.
Build against the local CUDA 12.9/NVSHMEM environment (no build isolation), from this module directory:
cd liger_cute_kernels
python -m pip install apache-tvm-ffi nvidia-nvshmem-cu12==3.6.5
LIGER_CUTE_CUDA_ARCHS=90a,100f \
pip wheel . --no-deps --no-build-isolation -w dist
# Release build:
# -> dist/liger_cute_kernels-<liger-version>-py3-none-manylinux_2_35_x86_64.whl
For a Hopper-only CUDA 13 build, select that toolkit's nvcc, install
nvidia-nvshmem-cu13==3.6.5 in a separate environment, and use
LIGER_CUTE_CUDA_ARCHS=90a instead.
NVSHMEM must be installed before compiling from source; pip's extras are
runtime dependencies and do not provision native build prerequisites before
the wheel-build step. After provisioning the prerequisites, a source install
can use pip install --no-build-isolation ".[cu12]" or ".[cu13]".
For a native NVSHMEM install:
NVSHMEM_HOME=/usr/local/nvshmem \
LIGER_CUTE_ENABLE_SM90_NONRDC_MOE=1 \
pip wheel . --no-deps --no-build-isolation -w dist
For the PyPI NVSHMEM layout:
pip install nvidia-nvshmem-cu12==3.6.5
pip wheel . --no-deps --no-build-isolation -w dist
The wheel reads its version from the repository's root pyproject.toml, so
liger-kernel and liger-cute-kernels are released with the same version. To
reuse architecture cores built in separate jobs, place them under
<core-dir>/90a/libliger_cute_kernels.so and
<core-dir>/100f/libliger_cute_kernels.so, then point packaging at the root:
LIGER_CUTE_CUDA_ARCHS=90a,100f \
LIGER_CUTE_CORE_DIR=/abs/dir-with-core \
pip wheel . --no-deps --no-build-isolation -w dist
Install order at the consumer side: the liger_kernel wheel first, then
optionally the matching native wheel.
The wheel workflow also runs directly on every push to main as a nightly
integration build. These runs build, package, validate, and retain the wheel as
a workflow artifact, but the PyPI upload job runs only for a published GitHub
release.
Source-tree Python verification
The Python facade imports the external tvm_ffi package, preloads NVSHMEM from
nvidia/nvshmem/lib, and selects the packaged SM90a or SM100f core from the
active GPU capability. In a source checkout, pytest skips the Python tests
until the required pieces exist.
cd liger_cute_kernels
python -m pip install apache-tvm-ffi
cmake -S . -B build/core \
-DLIGER_CUTE_BUILD_BINDINGS=OFF \
-DCMAKE_BUILD_TYPE=Release -GNinja
cmake --build build/core --target liger_cute_kernels -j
cp build/core/csrc/core/libliger_cute_kernels.so \
liger_cute_kernels/libliger_cute_kernels_sm90a.so
python - <<'PY'
import liger_cute_kernels.tvm_ffi as tvm_ffi
print(tvm_ffi.is_available())
print(tvm_ffi.uniqueid_nbytes())
PY
python -m pytest -q test
Alternatively, install the built wheel and its dependencies.
CMake options
| Option | Default | Effect |
|---|---|---|
LIGER_CUTE_BUILD_BINDINGS |
OFF |
Deprecated compatibility option. Leave OFF; tensor APIs are exposed through TVM FFI only. |
LIGER_CUTE_CORE_IMPORTED_DIR |
(empty) | Dir holding a prebuilt libliger_cute_kernels.so. When set, the core is linked as an imported library (not compiled) and CUTLASS is not required. |
LIGER_CUTE_CUDA_ARCH |
90a |
CUDA target architecture. Use 100f for one B200/B300 Blackwell-family build. |
LIGER_CUTE_CUDA_ARCHS |
(empty) | Comma-separated architecture cores to package in one wheel, for example 90a,100f. Mutually exclusive with LIGER_CUTE_CUDA_ARCH. |
LIGER_CUTE_VERSION |
root package version | Explicit wheel-version override; release builds validate it against the root liger-kernel version. |
LIGER_CUTE_STRIP_NATIVE |
0 |
Set to 1 to strip packaged core libraries. |
LIGER_CUTE_FSLCE_SM100_STAGES |
5 |
SM100 forward TMA mainloop stages. |
LIGER_CUTE_FSLCE_SM100_WAVE_N_TILES |
64 |
SM100 forward communication wave width in N256 tiles. |
LIGER_CUTE_FSLCE_SM100_BACKWARD_STAGES |
5 |
SM100 fused backward dZ TMA mainloop stages. |
LIGER_CUTE_FSLCE_SM100_BACKWARD_DIAGNOSTIC_CHUNK_PIPELINE |
OFF |
Diagnostic only: force the chunk-granular deferred dX schedule on a single host so the state machine can be validated without an inter-host ring. |
LIGER_CUTE_BUILD_TESTS |
OFF |
Build the C++ gtest tests. |
LIGER_CUTE_TESTS_ONLY |
OFF |
Build only tests; skips NVSHMEM/TVM FFI/core packaging. |
LIGER_CUTE_STATIC_LIBSTDCXX |
ON |
Statically link libstdc++/libgcc into the core so its internal C++ ABI is invisible to consumers. |
NVSHMEM_HOME |
/usr/local/nvshmem |
NVSHMEM install root (also read from the env var). |
CUTLASS_HOME |
(env) | CUTLASS repo root (read from the env var; only needed to compile the core). |
Standard CMake flags also apply: -DCMAKE_BUILD_TYPE=Release, -GNinja,
-DPython_EXECUTABLE=....
CUDA architecture defaults to sm_90a (Hopper, with WGMMA/TMA/multicast) and
is configurable with -DLIGER_CUTE_CUDA_ARCH=100f for B200 and B300.
Environment variables
| Variable | Used by | Effect |
|---|---|---|
NVSHMEM_HOME |
CMake / cute_build |
NVSHMEM install root (default /usr/local/nvshmem). |
CUTLASS_HOME |
CMake | CUTLASS repo root (core compile only). |
LIGER_CUTE_CUDA_ARCH |
CMake / cute_build |
CUDA target architecture; use 100f for a shared Blackwell-family build. |
LIGER_CUTE_CUDA_ARCHS |
cute_build |
Comma-separated architecture cores to package in one wheel, for example 90a,100f. |
LIGER_CUTE_CORE_DIR |
setup.py |
Dir with a prebuilt core. Set → link it (no core recompile); unset → build the core from source. |
LIGER_CUTE_BUILD_JOBS |
cute_build |
Maximum parallel native build jobs. The release workflow uses 4. |
LIGER_CUTE_VERSION |
setup.py |
Override the root liger-kernel version used by the native wheel. |
LIGER_CUTE_STRIP_NATIVE |
cute_build |
Set to 1 to strip packaged native libraries. |
LIGER_CUTE_WHEEL_PLATFORM_TAG |
setup.py |
Override the native wheel platform tag. The pinned release container uses manylinux_2_35_x86_64. |
Verifying a core build
SO=build/core/csrc/core/libliger_cute_kernels.so
nm -D --defined-only "$SO" | grep ' T ' # exports: only liger_cute_*
readelf -d "$SO" | grep NEEDED # no libtorch, no direct libstdc++
The core should export only liger_cute_* symbols and have no direct
libstdc++/libtorch NEEDED entries (a transitive libstdc++ via
libnvshmem_host is expected and harmless).
Runtime notes
tvm_ffi.pyselectslibliger_cute_kernels_sm90a.sofor H100/H200 andlibliger_cute_kernels_sm100f.sofor the Blackwell family, then loads it withtvm_ffi.load_module.- NVSHMEM is not copied into the LCK wheel. The loader preloads
libnvshmem_host.so.3from the separately installednvidia-nvshmem-cu12==3.6.5ornvidia-nvshmem-cu13==3.6.5package, matching the native core's build toolkit.






