[CUDA] Add Hopper tensor-parallel fused linear cross entropy (#1422)

## Summary

Adds Hopper tensor-parallel fused linear scaled cross entropy
forward/backward with NVLS, direct-peer, and hierarchical two-host
paths.

Production choices:
- zero-spill cluster-2 M-fast dZ handoff with compact precomputed
coefficients;
- zero-spill four-stage combined dX+dW with one reused R24 accumulator;
- H2048 dX split-K2 when the local vocabulary has an even K64 tile
count,
  otherwise split-K1; H4096 uses split-K1;
- N32 FP32 TMA dX publication, N64/S2 dW epilogue, final-only
publication fence;
- post-compute local/hierarchical FP32-to-BF16 finalization;
- device-resident graph-safe launch epochs with one stream-ordered tail
barrier;
- non-RDC WGMMA translation units with remote NVSHMEM code isolated in
RDC sources.

## Tensor-parallel reduction API

Reduction transport is now reusable infrastructure under
`liger_cute::detail`:

- `TpReducePlan` selects the local NVLS or DirectPeer backend on the
host.
- Kernels receive only the selected compact device view; the full host
plan is never passed to device code.
- `RequiresRemote` is a launcher template argument, so the local path
carries no runtime remote branch or remote fields.
- Two-host execution is local NVLS reduce-scatter, explicit
`launch_remote_reduce(...)`, then the common local all-gather/scatter.
- DirectPeer remains supported behind the same local reduction API.

The refactor removes the unused pre-cluster dZ/role framework and old
non-cluster hierarchical implementation. Net change: 1,907 fewer lines
while preserving the validated kernel schedules.

## Kernel structure

### Warp functions and roles

**Forward `forward_gemm_kernel_sm90` (12 warps, cluster-2)**
- Warp 0 is the TMA producer for X and weight panels.
- Warps 1-3 are reserved/inactive.
- Warp groups 1-2 (8 warps) execute the N320 logical GEMM as two N160
accumulator banks, then perform the N80/N40 online-softmax and entropy
fold.
- Cluster peers share the token-side input while owning adjacent
vocabulary work.

**Backward dZ handoff (12 warps, cluster-2)**
- Warp 3 drives the three-stage TMA pipeline for X/W panels.
- Warps 0-2 consume handed-off FP16 logits, apply precomputed row
coefficients, target correction, BF16 conversion, and TMA output stores.
- Warp groups 1-2 execute M128xN256xK64 WGMMA and hand the rounded FP16
logits through shared memory.
- The handoff separates accumulator and epilogue liveness, eliminating
the old dZ spill path.

**Backward combined dX+dW (12 warps, cluster-2)**
- Warp 0 drives the continuous four-stage TMA producer pipeline.
- Two communication warps consume the FP32 dX ring, perform local NVLS
reduction/publication, and overlap with dW.
- Warp groups 1-2 execute dX then dW using one explicitly reused
M128xN256 FP32 accumulator bank at R24.
- dX uses W multicast and N32 FP32 TMA publication. dW uses dZ
multicast, independent X loads, and N64/S2 TMA store/reduce-add.
- H2048 uses split-K2 with atomic accumulation into a stream-cleared
durable
  buffer when its K64 tile count is even; odd tile counts and H4096 use
  split-K1.

### Forward launch sequence

1. `forward_gemm_kernel_sm90`: local GEMM plus online
max/sum/entropy/target partials.
2. `forward_split_reduce_kernel_sm90`: reduce local vocabulary splits.
3. Tensor-parallel MAX reduction.
4. `pack_forward_reduction_kernel`: correct/pack sum fields.
5. Tensor-parallel SUM reduction.
6. `finalize_forward_kernel`: produce log-probability, LSE, and optional
entropy.

### Backward launch sequence

For each token wave:
1. `backward_dz_handoff_wave_kernel_sm90`: logits GEMM, compact dZ math,
BF16 dZ workspace.
2. `backward_dx_cluster2_gemm_wave_kernel_sm90`: continuous four-stage
dX then dW compute with overlapped local communication.

After all waves:
3. Single host: local FP32 reduction result to BF16 scatter.
4. Hierarchical: separate-RDC inter-host reduction, local NVLS
broadcast/all-gather, then BF16 scatter.
5. Direct-peer systems use the post-compute fallback finalizer.
6. One stream-ordered tensor-parallel tail barrier prevents staging
reuse before peers finish.

### Hoisted versus repeated work

**Hoisted**
- Backend capability/team selection, workspace reservation, rank
mapping, and TMA descriptor construction occur once on the host.
- TMA descriptors are prefetched once per CTA role before persistent
tile loops.
- Shape counts, raster bounds, and split-K selection are computed
outside inner K loops.
- dZ stores per-row `exp_bias`, `correction_bias`, and
`correction_slope`; row coefficients/target are hoisted outside the
inner column loop.
- dX and dW share one accumulator object and aliased A/B shared-memory
arenas.
- IB communication and final BF16 materialization remain outside the hot
combined WGMMA kernel.

**Not hoisted**
- Per-wave dZ and combined dX+dW launches.
- Per-tile TMA loads, WGMMA issue, accumulator-to-SMEM conversion, and
dX publication.
- Per-element dZ `exp2`, entropy correction, and target subtraction.
- Per-stage NVLS ring synchronization/reduction and split-K2 atomic
accumulation.
- dW N64 TMA store/reduce-add for each output subtile and wave.

## Validation

- `make checkstyle`
- clean CMake shared-library build
- C++ tests: 8/8 passed
- Python WORLD and strided-subteam forward/backward tests: 2/2 passed,
including two CUDA Graph replays
- backward TP1/2/4/8 selftests: entropy on/off, ragged shapes,
multi-wave/full-grid, TPR1/2/4
- poison staging/dZ and delayed communication warp tests
- forward TP1/2/4/8 selftests: entropy on/off, ragged, temperature,
large split
- hierarchical TP16 forward/backward
- benchmarked kernel schedule before the transport-only API refactor:
92/92 unique matched points passed in both entropy modes; TP1/2/4/8/16
- post-refactor: checkstyle and focused Python facade regression passed;
GPU compilation remains delegated to PR validation

## Benchmark methodology

H200 SXM, CUDA 12.9, 8 warmups and 30 measured iterations, p50 shown
below. TFLOP/s are per GPU unless labeled aggregate. Liger entropy-off
is compared with Verl's no-entropy configuration. Memory factor is `Verl
GiB / Liger GiB`; values above 1 mean Liger uses less memory.
Allocation-free means no hot-path allocation; Liger's reported footprint
includes pre-reserved symmetric communication workspaces. H100 and H200
absolute results are kept separate.

## Overall Liger vs Verl

| Pass | Matched points | Geomean speedup | Range | Slower points |
Geomean mem V/L |
|---|---:|---:|---:|---:|---:|
| backward | 46 | 1.54x | 0.95-1.87x | 1 | 0.82x |
| forward | 46 | 2.11x | 1.69-3.79x | 0 | 0.47x |

Forward is faster at every matched point. Backward is faster at every
matched point except M8192/H2048/V131072 at TP16.

## Strong-scaling efficiency

Efficiency is per-GPU TFLOP/s at TPn divided by TP1 per-GPU TFLOP/s for
the same global workload.

| Pass | M | H | V | TP1 TF/GPU | TP2 eff. | TP4 eff. | TP8 eff. | TP16
eff. |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| backward | 2048 | 4096 | 131072 | 767.9 | 98.1% | 92.7% | 82.5% |
50.1% |
| backward | 8192 | 2048 | 131072 | 694.9 | 91.4% | 82.8% | 68.6% |
37.8% |
| backward | 8192 | 4096 | 131072 | 762.8 | 97.8% | 93.7% | 84.0% |
54.5% |
| backward | 16384 | 4096 | 131072 | 761.1 | 97.3% | 93.8% | 83.7% |
55.3% |
| backward | 2048 | 4096 | 262144 | 783.1 | 98.2% | 97.0% | 91.1% |
66.8% |
| backward | 8192 | 2048 | 262144 | 711.3 | 96.7% | 92.3% | 85.0% |
58.2% |
| backward | 8192 | 4096 | 262144 | 777.4 | 98.4% | 96.6% | 91.8% |
70.6% |
| backward | 16384 | 4096 | 262144 | 775.8 | 98.5% | 95.8% | 91.9% |
71.2% |
| forward | 2048 | 4096 | 131072 | 787.7 | 99.9% | 97.7% | 83.5% | 58.4%
|
| forward | 8192 | 2048 | 131072 | 747.7 | 99.4% | 96.7% | 90.4% | 79.1%
|
| forward | 8192 | 4096 | 131072 | 789.0 | 99.7% | 98.8% | 95.9% | 88.9%
|
| forward | 16384 | 4096 | 131072 | 757.4 | 99.7% | 98.3% | 95.0% |
91.4% |
| forward | 2048 | 4096 | 262144 | 791.1 | 99.0% | 100.0% | 95.4% |
77.0% |
| forward | 8192 | 2048 | 262144 | 747.5 | 99.7% | 99.1% | 95.6% | 86.8%
|
| forward | 8192 | 4096 | 262144 | 788.3 | 99.9% | 100.1% | 98.3% |
94.1% |
| forward | 16384 | 4096 | 262144 | 755.3 | 100.0% | 99.8% | 98.3% |
93.9% |

## Weak-scaling efficiency

Global vocabulary grows with TP while local vocabulary remains 65,536.
Efficiency is per-GPU TFLOP/s relative to TP1.

| Pass | H | TP1 TF/GPU | TP2 eff. | TP4 eff. | TP8 eff. | TP16 eff. |
|---|---:|---:|---:|---:|---:|---:|
| backward | 2048 | 661.4 | 96.1% | 99.2% | 99.2% | 94.7% |
| backward | 4096 | 757.6 | 98.4% | 99.1% | 98.8% | 95.8% |
| forward | 2048 | 748.2 | 99.4% | 99.0% | 98.5% | 97.4% |
| forward | 4096 | 788.5 | 99.7% | 100.1% | 99.3% | 98.4% |

## Full Liger vs Verl results

| Pass | M | H | V | TP | Liger TF/GPU | Verl TF/GPU | Speedup | Liger
GiB | Verl GiB | Mem V/L |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| backward | 8192 | 2048 | 65536 | 1 | 661.4 | 375.2 | 1.76x | 2.26 |
1.15 | 0.51x |
| backward | 8192 | 4096 | 65536 | 1 | 757.6 | 507.3 | 1.49x | 2.82 |
1.99 | 0.71x |
| backward | 2048 | 4096 | 131072 | 1 | 767.9 | 445.2 | 1.72x | 3.85 |
3.14 | 0.82x |
| backward | 2048 | 4096 | 131072 | 2 | 753.3 | 436.4 | 1.73x | 2.72 |
1.65 | 0.61x |
| backward | 2048 | 4096 | 131072 | 4 | 711.6 | 385.0 | 1.85x | 2.16 |
0.88 | 0.41x |
| backward | 2048 | 4096 | 131072 | 8 | 633.3 | 362.6 | 1.75x | 1.88 |
0.52 | 0.28x |
| backward | 2048 | 4096 | 131072 | 16 | 385.0 | 311.3 | 1.24x | 0.17 |
0.33 | 1.94x |
| backward | 8192 | 2048 | 131072 | 1 | 694.9 | 378.9 | 1.83x | 2.88 |
1.89 | 0.66x |
| backward | 8192 | 2048 | 131072 | 2 | 635.4 | 372.0 | 1.71x | 2.26 |
1.15 | 0.51x |
| backward | 8192 | 2048 | 131072 | 4 | 575.2 | 329.3 | 1.75x | 1.94 |
0.71 | 0.37x |
| backward | 8192 | 2048 | 131072 | 8 | 476.9 | 313.8 | 1.52x | 1.79 |
0.56 | 0.31x |
| backward | 8192 | 2048 | 131072 | 16 | 262.4 | 277.1 | 0.95x | 0.14 |
0.49 | 3.50x |
| backward | 8192 | 4096 | 131072 | 1 | 762.8 | 510.6 | 1.49x | 3.94 |
3.48 | 0.88x |
| backward | 8192 | 4096 | 131072 | 2 | 745.7 | 501.2 | 1.49x | 2.82 |
1.99 | 0.71x |
| backward | 8192 | 4096 | 131072 | 4 | 714.7 | 446.2 | 1.60x | 2.26 |
1.18 | 0.52x |
| backward | 8192 | 4096 | 131072 | 8 | 640.6 | 431.4 | 1.48x | 1.97 |
0.84 | 0.43x |
| backward | 8192 | 4096 | 131072 | 16 | 415.7 | 387.3 | 1.07x | 0.27 |
0.68 | 2.52x |
| backward | 16384 | 4096 | 131072 | 1 | 761.1 | 519.8 | 1.46x | 4.07 |
3.93 | 0.97x |
| backward | 16384 | 4096 | 131072 | 2 | 740.2 | 510.8 | 1.45x | 2.94 |
2.46 | 0.84x |
| backward | 16384 | 4096 | 131072 | 4 | 714.1 | 458.3 | 1.56x | 2.38 |
1.58 | 0.66x |
| backward | 16384 | 4096 | 131072 | 8 | 637.1 | 444.4 | 1.43x | 2.10 |
1.28 | 0.61x |
| backward | 16384 | 4096 | 131072 | 16 | 421.0 | 404.6 | 1.04x | 0.39 |
1.13 | 2.90x |
| backward | 2048 | 4096 | 262144 | 1 | 783.1 | 449.4 | 1.74x | 6.10 |
6.14 | 1.01x |
| backward | 2048 | 4096 | 262144 | 2 | 768.8 | 444.1 | 1.73x | 3.85 |
3.14 | 0.82x |
| backward | 2048 | 4096 | 262144 | 4 | 759.7 | 435.4 | 1.74x | 2.72 |
1.65 | 0.61x |
| backward | 2048 | 4096 | 262144 | 8 | 713.6 | 383.2 | 1.86x | 2.16 |
0.88 | 0.41x |
| backward | 2048 | 4096 | 262144 | 16 | 523.4 | 351.2 | 1.49x | 0.31 |
0.52 | 1.68x |
| backward | 8192 | 2048 | 262144 | 1 | 711.3 | 381.3 | 1.87x | 4.13 |
3.35 | 0.81x |
| backward | 8192 | 2048 | 262144 | 2 | 687.8 | 376.7 | 1.83x | 2.88 |
1.89 | 0.66x |
| backward | 8192 | 2048 | 262144 | 4 | 656.4 | 371.3 | 1.77x | 2.26 |
1.15 | 0.51x |
| backward | 8192 | 2048 | 262144 | 8 | 604.6 | 329.0 | 1.84x | 1.94 |
0.71 | 0.37x |
| backward | 8192 | 2048 | 262144 | 16 | 414.0 | 306.1 | 1.35x | 0.22 |
0.56 | 2.55x |
| backward | 8192 | 4096 | 262144 | 1 | 777.4 | 512.1 | 1.52x | 6.19 |
6.45 | 1.04x |
| backward | 8192 | 4096 | 262144 | 2 | 765.1 | 505.7 | 1.51x | 3.94 |
3.48 | 0.88x |
| backward | 8192 | 4096 | 262144 | 4 | 751.1 | 500.6 | 1.50x | 2.82 |
1.99 | 0.71x |
| backward | 8192 | 4096 | 262144 | 8 | 714.0 | 447.2 | 1.60x | 2.26 |
1.18 | 0.52x |
| backward | 8192 | 4096 | 262144 | 16 | 548.6 | 421.2 | 1.30x | 0.41 |
0.84 | 2.05x |
| backward | 16384 | 4096 | 262144 | 1 | 775.8 | 520.5 | 1.49x | 6.32 |
6.87 | 1.09x |
| backward | 16384 | 4096 | 262144 | 2 | 763.9 | 516.4 | 1.48x | 4.07 |
3.93 | 0.97x |
| backward | 16384 | 4096 | 262144 | 4 | 743.2 | 509.9 | 1.46x | 2.94 |
2.46 | 0.84x |
| backward | 16384 | 4096 | 262144 | 8 | 713.2 | 458.0 | 1.56x | 2.38 |
1.58 | 0.66x |
| backward | 16384 | 4096 | 262144 | 16 | 552.6 | 429.2 | 1.29x | 0.53 |
1.28 | 2.42x |
| backward | 8192 | 2048 | 524288 | 8 | 655.9 | 369.7 | 1.77x | 2.26 |
1.15 | 0.51x |
| backward | 8192 | 4096 | 524288 | 8 | 748.4 | 499.0 | 1.50x | 2.82 |
1.99 | 0.71x |
| backward | 8192 | 2048 | 1048576 | 16 | 626.5 | 366.9 | 1.71x | 0.69 |
1.15 | 1.67x |
| backward | 8192 | 4096 | 1048576 | 16 | 726.1 | 494.5 | 1.47x | 1.25 |
1.99 | 1.59x |
| forward | 8192 | 2048 | 65536 | 1 | 748.2 | 361.7 | 2.07x | 0.28 |
0.29 | 1.04x |
| forward | 8192 | 4096 | 65536 | 1 | 788.5 | 417.4 | 1.89x | 0.56 |
0.57 | 1.02x |
| forward | 2048 | 4096 | 131072 | 1 | 787.7 | 460.7 | 1.71x | 1.02 |
1.02 | 1.00x |
| forward | 2048 | 4096 | 131072 | 2 | 786.9 | 439.2 | 1.79x | 0.98 |
0.52 | 0.53x |
| forward | 2048 | 4096 | 131072 | 4 | 769.3 | 408.7 | 1.88x | 0.73 |
0.27 | 0.37x |
| forward | 2048 | 4096 | 131072 | 8 | 657.7 | 351.8 | 1.87x | 0.60 |
0.14 | 0.23x |
| forward | 2048 | 4096 | 131072 | 16 | 460.3 | 253.4 | 1.82x | 0.46 |
0.08 | 0.17x |
| forward | 8192 | 2048 | 131072 | 1 | 747.7 | 226.1 | 3.31x | 0.53 |
0.55 | 1.04x |
| forward | 8192 | 2048 | 131072 | 2 | 743.4 | 199.3 | 3.73x | 0.74 |
0.29 | 0.39x |
| forward | 8192 | 2048 | 131072 | 4 | 722.7 | 343.3 | 2.11x | 0.62 |
0.16 | 0.26x |
| forward | 8192 | 2048 | 131072 | 8 | 676.0 | 321.2 | 2.10x | 0.56 |
0.10 | 0.18x |
| forward | 8192 | 2048 | 131072 | 16 | 591.1 | 251.8 | 2.35x | 0.45 |
0.06 | 0.13x |
| forward | 8192 | 4096 | 131072 | 1 | 789.0 | 224.1 | 3.52x | 1.06 |
1.08 | 1.02x |
| forward | 8192 | 4096 | 131072 | 2 | 786.3 | 411.8 | 1.91x | 1.03 |
0.57 | 0.55x |
| forward | 8192 | 4096 | 131072 | 4 | 779.4 | 401.0 | 1.94x | 0.78 |
0.32 | 0.41x |
| forward | 8192 | 4096 | 131072 | 8 | 756.8 | 388.6 | 1.95x | 0.65 |
0.19 | 0.29x |
| forward | 8192 | 4096 | 131072 | 16 | 701.1 | 333.9 | 2.10x | 0.51 |
0.13 | 0.25x |
| forward | 16384 | 4096 | 131072 | 1 | 757.4 | 372.8 | 2.03x | 1.13 |
1.16 | 1.03x |
| forward | 16384 | 4096 | 131072 | 2 | 754.9 | 365.0 | 2.07x | 1.09 |
0.64 | 0.59x |
| forward | 16384 | 4096 | 131072 | 4 | 744.7 | 358.3 | 2.08x | 0.84 |
0.38 | 0.45x |
| forward | 16384 | 4096 | 131072 | 8 | 719.9 | 331.1 | 2.17x | 0.72 |
0.25 | 0.35x |
| forward | 16384 | 4096 | 131072 | 16 | 692.2 | 327.0 | 2.12x | 0.57 |
0.19 | 0.33x |
| forward | 2048 | 4096 | 262144 | 1 | 791.1 | 468.7 | 1.69x | 2.02 |
2.02 | 1.00x |
| forward | 2048 | 4096 | 262144 | 2 | 783.1 | 422.3 | 1.85x | 1.48 |
1.02 | 0.69x |
| forward | 2048 | 4096 | 262144 | 4 | 790.9 | 437.6 | 1.81x | 0.98 |
0.52 | 0.53x |
| forward | 2048 | 4096 | 262144 | 8 | 754.4 | 404.2 | 1.87x | 0.73 |
0.27 | 0.37x |
| forward | 2048 | 4096 | 262144 | 16 | 609.5 | 327.5 | 1.86x | 0.52 |
0.14 | 0.27x |
| forward | 8192 | 2048 | 262144 | 1 | 747.5 | 372.3 | 2.01x | 1.03 |
1.06 | 1.03x |
| forward | 8192 | 2048 | 262144 | 2 | 745.5 | 360.5 | 2.07x | 0.99 |
0.55 | 0.56x |
| forward | 8192 | 2048 | 262144 | 4 | 740.7 | 353.1 | 2.10x | 0.74 |
0.29 | 0.39x |
| forward | 8192 | 2048 | 262144 | 8 | 714.4 | 341.1 | 2.09x | 0.62 |
0.16 | 0.26x |
| forward | 8192 | 2048 | 262144 | 16 | 648.8 | 309.6 | 2.10x | 0.48 |
0.10 | 0.21x |
| forward | 8192 | 4096 | 262144 | 1 | 788.3 | 208.0 | 3.79x | 2.06 |
2.09 | 1.01x |
| forward | 8192 | 4096 | 262144 | 2 | 787.6 | 233.2 | 3.38x | 1.53 |
1.08 | 0.71x |
| forward | 8192 | 4096 | 262144 | 4 | 789.0 | 411.4 | 1.92x | 1.03 |
0.57 | 0.55x |
| forward | 8192 | 4096 | 262144 | 8 | 775.0 | 401.4 | 1.93x | 0.78 |
0.32 | 0.41x |
| forward | 8192 | 4096 | 262144 | 16 | 741.9 | 379.7 | 1.95x | 0.57 |
0.19 | 0.33x |
| forward | 16384 | 4096 | 262144 | 1 | 755.3 | 374.1 | 2.02x | 2.13 |
2.19 | 1.03x |
| forward | 16384 | 4096 | 262144 | 2 | 755.0 | 367.4 | 2.05x | 1.59 |
1.16 | 0.73x |
| forward | 16384 | 4096 | 262144 | 4 | 753.8 | 364.0 | 2.07x | 1.09 |
0.64 | 0.59x |
| forward | 16384 | 4096 | 262144 | 8 | 742.1 | 357.1 | 2.08x | 0.84 |
0.38 | 0.45x |
| forward | 16384 | 4096 | 262144 | 16 | 709.3 | 344.8 | 2.06x | 0.64 |
0.25 | 0.39x |
| forward | 8192 | 2048 | 524288 | 8 | 736.9 | 351.5 | 2.10x | 0.74 |
0.29 | 0.39x |
| forward | 8192 | 4096 | 524288 | 8 | 782.6 | 410.2 | 1.91x | 1.03 |
0.57 | 0.55x |
| forward | 8192 | 2048 | 1048576 | 16 | 729.1 | 347.2 | 2.10x | 0.66 |
0.29 | 0.44x |
| forward | 8192 | 4096 | 1048576 | 16 | 776.0 | 407.4 | 1.90x | 0.95 |
0.57 | 0.60x |

## Liger entropy-on results

| Pass | M | H | V | TP | p50 ms | TF/GPU | Aggregate TF | GiB | Backend
| split-K |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---|---:|
| backward | 2048 | 4096 | 131072 | 1 | 8.589 | 768.1 | 768.1 | 3.85 |
nvls | 1 |
| backward | 2048 | 4096 | 131072 | 2 | 4.395 | 750.4 | 1500.9 | 2.72 |
nvls | 1 |
| backward | 2048 | 4096 | 131072 | 4 | 2.311 | 713.7 | 2854.6 | 2.16 |
nvls | 1 |
| backward | 2048 | 4096 | 131072 | 8 | 1.291 | 638.5 | 5108.2 | 1.88 |
nvls | 1 |
| backward | 2048 | 4096 | 131072 | 16 | 1.066 | 386.9 | 6190.2 | 0.17 |
hierarchical | 1 |
| backward | 2048 | 4096 | 262144 | 1 | 16.842 | 783.4 | 783.4 | 6.10 |
nvls | 1 |
| backward | 2048 | 4096 | 262144 | 2 | 8.570 | 769.8 | 1539.6 | 3.85 |
nvls | 1 |
| backward | 2048 | 4096 | 262144 | 4 | 4.366 | 755.6 | 3022.2 | 2.72 |
nvls | 1 |
| backward | 2048 | 4096 | 262144 | 8 | 2.306 | 715.1 | 5720.5 | 2.16 |
nvls | 1 |
| backward | 2048 | 4096 | 262144 | 16 | 1.574 | 523.8 | 8380.4 | 0.31 |
hierarchical | 1 |
| backward | 8192 | 2048 | 65536 | 1 | 10.008 | 659.2 | 659.2 | 2.26 |
nvls | 2 |
| backward | 8192 | 2048 | 131072 | 1 | 19.086 | 691.3 | 691.3 | 2.88 |
nvls | 2 |
| backward | 8192 | 2048 | 131072 | 2 | 10.421 | 633.0 | 1266.1 | 2.26 |
nvls | 2 |
| backward | 8192 | 2048 | 131072 | 4 | 5.759 | 572.8 | 2291.2 | 1.94 |
nvls | 2 |
| backward | 8192 | 2048 | 131072 | 8 | 3.467 | 475.7 | 3805.4 | 1.79 |
nvls | 2 |
| backward | 8192 | 2048 | 131072 | 16 | 3.141 | 262.6 | 4201.1 | 0.14 |
hierarchical | 2 |
| backward | 8192 | 2048 | 262144 | 1 | 37.342 | 706.7 | 706.7 | 4.13 |
nvls | 2 |
| backward | 8192 | 2048 | 262144 | 2 | 19.290 | 684.0 | 1368.0 | 2.88 |
nvls | 2 |
| backward | 8192 | 2048 | 262144 | 4 | 10.105 | 652.8 | 2611.3 | 2.26 |
nvls | 2 |
| backward | 8192 | 2048 | 262144 | 8 | 5.454 | 604.8 | 4838.6 | 1.94 |
nvls | 2 |
| backward | 8192 | 2048 | 262144 | 16 | 3.999 | 412.4 | 6598.0 | 0.22 |
hierarchical | 2 |
| backward | 8192 | 2048 | 524288 | 8 | 10.070 | 655.1 | 5241.0 | 2.26 |
nvls | 2 |
| backward | 8192 | 2048 | 1048576 | 16 | 10.573 | 624.0 | 9983.4 | 0.69
| hierarchical | 2 |
| backward | 8192 | 4096 | 65536 | 1 | 17.420 | 757.4 | 757.4 | 2.82 |
nvls | 1 |
| backward | 8192 | 4096 | 131072 | 1 | 34.601 | 762.6 | 762.6 | 3.94 |
nvls | 1 |
| backward | 8192 | 4096 | 131072 | 2 | 17.774 | 742.3 | 1484.7 | 2.82 |
nvls | 1 |
| backward | 8192 | 4096 | 131072 | 4 | 9.214 | 716.0 | 2863.9 | 2.26 |
nvls | 1 |
| backward | 8192 | 4096 | 131072 | 8 | 5.153 | 640.2 | 5121.4 | 1.97 |
nvls | 1 |
| backward | 8192 | 4096 | 131072 | 16 | 3.963 | 416.2 | 6658.9 | 0.27 |
hierarchical | 1 |
| backward | 8192 | 4096 | 262144 | 1 | 67.858 | 777.8 | 777.8 | 6.19 |
nvls | 1 |
| backward | 8192 | 4096 | 262144 | 2 | 34.470 | 765.5 | 1531.1 | 3.94 |
nvls | 1 |
| backward | 8192 | 4096 | 262144 | 4 | 17.560 | 751.4 | 3005.5 | 2.82 |
nvls | 1 |
| backward | 8192 | 4096 | 262144 | 8 | 9.209 | 716.4 | 5731.1 | 2.26 |
nvls | 1 |
| backward | 8192 | 4096 | 262144 | 16 | 6.014 | 548.5 | 8775.6 | 0.41 |
hierarchical | 1 |
| backward | 8192 | 4096 | 524288 | 8 | 17.647 | 747.7 | 5981.2 | 2.82 |
nvls | 1 |
| backward | 8192 | 4096 | 1048576 | 16 | 18.181 | 725.7 | 11611.2 |
1.25 | hierarchical | 1 |
| backward | 16384 | 4096 | 131072 | 1 | 69.235 | 762.3 | 762.3 | 4.07 |
nvls | 1 |
| backward | 16384 | 4096 | 131072 | 2 | 35.679 | 739.6 | 1479.2 | 2.94
| nvls | 1 |
| backward | 16384 | 4096 | 131072 | 4 | 18.499 | 713.2 | 2853.0 | 2.38
| nvls | 1 |
| backward | 16384 | 4096 | 131072 | 8 | 10.382 | 635.4 | 5083.3 | 2.10
| nvls | 1 |
| backward | 16384 | 4096 | 131072 | 16 | 7.832 | 421.1 | 6738.3 | 0.39
| hierarchical | 1 |
| backward | 16384 | 4096 | 262144 | 1 | 136.168 | 775.2 | 775.2 | 6.32
| nvls | 1 |
| backward | 16384 | 4096 | 262144 | 2 | 69.172 | 763.0 | 1526.0 | 4.07
| nvls | 1 |
| backward | 16384 | 4096 | 262144 | 4 | 35.475 | 743.9 | 2975.4 | 2.94
| nvls | 1 |
| backward | 16384 | 4096 | 262144 | 8 | 18.444 | 715.4 | 5722.9 | 2.38
| nvls | 1 |
| backward | 16384 | 4096 | 262144 | 16 | 11.933 | 552.8 | 8845.5 | 0.53
| hierarchical | 1 |
| forward | 2048 | 4096 | 131072 | 1 | 2.842 | 773.9 | 773.9 | 1.02 |
nvls | |
| forward | 2048 | 4096 | 131072 | 2 | 1.392 | 789.8 | 1579.6 | 0.98 |
nvls | |
| forward | 2048 | 4096 | 131072 | 4 | 0.720 | 763.8 | 3055.3 | 0.73 |
nvls | |
| forward | 2048 | 4096 | 131072 | 8 | 0.424 | 648.0 | 5183.8 | 0.60 |
nvls | |
| forward | 2048 | 4096 | 131072 | 16 | 0.303 | 453.9 | 7262.3 | 0.46 |
hierarchical | |
| forward | 2048 | 4096 | 262144 | 1 | 5.612 | 783.6 | 783.6 | 2.02 |
nvls | |
| forward | 2048 | 4096 | 262144 | 2 | 2.820 | 779.7 | 1559.3 | 1.48 |
nvls | |
| forward | 2048 | 4096 | 262144 | 4 | 1.395 | 788.4 | 3153.5 | 0.98 |
nvls | |
| forward | 2048 | 4096 | 262144 | 8 | 0.736 | 746.5 | 5972.1 | 0.73 |
nvls | |
| forward | 2048 | 4096 | 262144 | 16 | 0.456 | 603.4 | 9653.6 | 0.52 |
hierarchical | |
| forward | 8192 | 2048 | 65536 | 1 | 2.957 | 743.6 | 743.6 | 0.28 |
nvls | |
| forward | 8192 | 2048 | 131072 | 1 | 5.899 | 745.5 | 745.5 | 0.53 |
nvls | |
| forward | 8192 | 2048 | 131072 | 2 | 2.973 | 739.7 | 1479.4 | 0.74 |
nvls | |
| forward | 8192 | 2048 | 131072 | 4 | 1.533 | 717.4 | 2869.6 | 0.62 |
nvls | |
| forward | 8192 | 2048 | 131072 | 8 | 0.815 | 674.2 | 5393.4 | 0.56 |
nvls | |
| forward | 8192 | 2048 | 131072 | 16 | 0.473 | 580.9 | 9294.3 | 0.45 |
hierarchical | |
| forward | 8192 | 2048 | 262144 | 1 | 11.777 | 746.9 | 746.9 | 1.03 |
nvls | |
| forward | 8192 | 2048 | 262144 | 2 | 5.918 | 743.1 | 1486.2 | 0.99 |
nvls | |
| forward | 8192 | 2048 | 262144 | 4 | 2.988 | 736.0 | 2944.0 | 0.74 |
nvls | |
| forward | 8192 | 2048 | 262144 | 8 | 1.548 | 710.5 | 5683.6 | 0.62 |
nvls | |
| forward | 8192 | 2048 | 262144 | 16 | 0.854 | 643.8 | 10300.6 | 0.48 |
hierarchical | |
| forward | 8192 | 2048 | 524288 | 8 | 3.002 | 732.6 | 5860.9 | 0.74 |
nvls | |
| forward | 8192 | 2048 | 1048576 | 16 | 3.045 | 722.1 | 11553.5 | 0.66
| hierarchical | |
| forward | 8192 | 4096 | 65536 | 1 | 5.596 | 785.9 | 785.9 | 0.56 |
nvls | |
| forward | 8192 | 4096 | 131072 | 1 | 11.182 | 786.7 | 786.7 | 1.06 |
nvls | |
| forward | 8192 | 4096 | 131072 | 2 | 5.575 | 788.9 | 1577.7 | 1.03 |
nvls | |
| forward | 8192 | 4096 | 131072 | 4 | 2.828 | 777.7 | 3110.7 | 0.78 |
nvls | |
| forward | 8192 | 4096 | 131072 | 8 | 1.458 | 754.1 | 6033.1 | 0.65 |
nvls | |
| forward | 8192 | 4096 | 131072 | 16 | 0.789 | 697.0 | 11151.2 | 0.51 |
hierarchical | |
| forward | 8192 | 4096 | 262144 | 1 | 22.375 | 786.2 | 786.2 | 2.06 |
nvls | |
| forward | 8192 | 4096 | 262144 | 2 | 11.141 | 789.5 | 1579.1 | 1.53 |
nvls | |
| forward | 8192 | 4096 | 262144 | 4 | 5.594 | 786.2 | 3144.9 | 1.03 |
nvls | |
| forward | 8192 | 4096 | 262144 | 8 | 2.833 | 776.2 | 6209.9 | 0.78 |
nvls | |
| forward | 8192 | 4096 | 262144 | 16 | 1.492 | 737.0 | 11791.4 | 0.57 |
hierarchical | |
| forward | 8192 | 4096 | 524288 | 8 | 5.645 | 779.2 | 6233.2 | 1.03 |
nvls | |
| forward | 8192 | 4096 | 1048576 | 16 | 5.677 | 774.7 | 12395.4 | 0.95
| hierarchical | |
| forward | 16384 | 4096 | 131072 | 1 | 23.312 | 754.6 | 754.6 | 1.13 |
nvls | |
| forward | 16384 | 4096 | 131072 | 2 | 11.685 | 752.8 | 1505.6 | 1.09 |
nvls | |
| forward | 16384 | 4096 | 131072 | 4 | 5.932 | 741.4 | 2965.4 | 0.84 |
nvls | |
| forward | 16384 | 4096 | 131072 | 8 | 3.067 | 716.9 | 5735.6 | 0.72 |
nvls | |
| forward | 16384 | 4096 | 131072 | 16 | 1.600 | 687.0 | 10992.6 | 0.57
| hierarchical | |
| forward | 16384 | 4096 | 262144 | 1 | 46.752 | 752.6 | 752.6 | 2.13 |
nvls | |
| forward | 16384 | 4096 | 262144 | 2 | 23.374 | 752.6 | 1505.3 | 1.59 |
nvls | |
| forward | 16384 | 4096 | 262144 | 4 | 11.702 | 751.7 | 3006.6 | 1.09 |
nvls | |
| forward | 16384 | 4096 | 262144 | 8 | 5.946 | 739.6 | 5917.2 | 0.84 |
nvls | |
| forward | 16384 | 4096 | 262144 | 16 | 3.116 | 705.7 | 11291.1 | 0.64
| hierarchical | |

## Resource results

- compact dZ handoff: 168 registers, zero spills, 11 barriers; dZ-only
p50 11.839 ms entropy-off and 11.898 ms entropy-on in the isolated
validation build.
- combined split-K1 and split-K2: 168 registers, zero spills, 7
barriers, 230,400 B dynamic shared memory; TPR1/2/4 validated.
- dX/dW mainloops are within 10% of tuned cuBLASLt; dX exceeds cuBLAS on
multicast-friendly shapes and dW is approximately 4% behind.

---------

Co-authored-by: jkolehma <[email protected]>
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Jari Kolehmainen
2026-08-31 20:15:17 +00:00
committed by GitHub
co-authored by jkolehma Copilot
parent f65ae6576f
commit 99747c53ba
38 changed files with 10731 additions and 5 deletions
+18 -4
View File
@@ -50,6 +50,21 @@ option(LIGER_CUTE_TESTS_ONLY
# ---------- Common CUDA / device dependencies (needed by core AND bindings) --
find_package(CUDAToolkit REQUIRED)
if(NOT LIGER_CUTE_TESTS_ONLY)
if(NOT DEFINED NCCL_HOME AND DEFINED ENV{NCCL_HOME})
set(NCCL_HOME "$ENV{NCCL_HOME}")
endif()
find_path(NCCL_INCLUDE_DIR NAMES nccl.h
HINTS "${NCCL_HOME}/include"
PATHS /usr/include /usr/local/cuda/include)
find_library(NCCL_LIBRARY NAMES nccl
HINTS "${NCCL_HOME}/lib" "${NCCL_HOME}/lib64"
PATHS /usr/lib /usr/lib64 /usr/lib/x86_64-linux-gnu)
if(NOT NCCL_INCLUDE_DIR OR NOT NCCL_LIBRARY)
message(FATAL_ERROR "NCCL not found (set NCCL_HOME=<prefix>)")
endif()
endif()
# The 'a' suffix enables architecture-accelerated features (WGMMA/TMA/multicast
# on sm_90a; tcgen05/UMMA on sm_100a). Override with -DLIGER_CUTE_CUDA_ARCH=100a
# for a Blackwell build. Strip any gencode flags a parent/toolchain may have
@@ -71,10 +86,9 @@ endif()
# Disable device-side assertions to avoid extern __assert_fail calls that cause
# ptxas to serialize WGMMA instructions.
add_compile_definitions(NDEBUG)
# NVSHMEM_ENABLE_ALL_DEVICE_INLINING is REQUIRED globally — see LigerCommKernels
# root CMakeLists for the full rationale (nvshmem.h device helpers reference
# device-state symbols gated on __CUDACC_RDC__; inlining makes non-RDC host-only
# TUs build cleanly).
# NVSHMEM_ENABLE_ALL_DEVICE_INLINING is the default for non-RDC host-only TUs:
# nvshmem.h device helpers otherwise reference state gated on __CUDACC_RDC__.
# RDC collective TUs explicitly undefine it when they need library device code.
add_compile_definitions(NVSHMEM_ENABLE_ALL_DEVICE_INLINING=ON)
# ── NVSHMEM ──────────────────────────────────────────────────────────────────
+35
View File
@@ -48,6 +48,41 @@ global batch size 512, and sequence length 4,096. Left: cross-entropy loss.
Right: per-step wall time. The paper excludes the initial CUDA Graph build from
its aggregate step-time statistics.
## Fused linear scaled cross entropy
The Hopper native core also provides
`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 two-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. Forward accepts the caller's tensor-parallel
`ncclComm_t` as an integer handle; backward accepts the NVSHMEM team configured
for the same tensor-parallel ranks. Both calls take inverse temperature.
Capacities for tokens, hidden size, local vocabulary, and reduction grouping
must be configured before capture or execution.
The facade owns NCCL communicator bootstrap helpers:
`nccl_get_unique_id`, `nccl_comm_init_rank`, and `nccl_comm_destroy`. Broadcast
the returned CPU `uint8` unique-ID tensor to the tensor-parallel ranks, create
one communicator per rank, and pass its integer handle to forward.
Hierarchical NVLS+remote backward currently requires the configured
tensor-parallel team to cover the full NVSHMEM world. Single-host NVLS
subgroups remain supported; a multi-host subgroup that is only part of a
larger world fails explicitly rather than communicating with nonmembers.
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.
Two-host execution is composed as local NVLS reduce-scatter, an explicit
RDC-compiled remote reduction follow-up, and the same local all-gather/scatter
used by single-host execution. Whether the remote follow-up is required is a
launcher template parameter, not a runtime kernel argument.
## Package architecture
Native CUDA build for the MoE port (from `LigerCommKernels`). The lck wheel
+106 -1
View File
@@ -20,15 +20,118 @@ list(FILTER LIGER_CUTE_CORE_SOURCES EXCLUDE REGEX "/src/moe/tune/")
# The legacy TensorView ABI is no longer part of libliger_cute_kernels.so.
# Tensor-carrying entry points are exported through TVM FFI.
list(FILTER LIGER_CUTE_CORE_SOURCES EXCLUDE REGEX "/src/tensor_view\\.cpp$")
# WGMMA/TMA translation units stay non-RDC. Remote NVSHMEM collectives are
# isolated in detail/remote_all_reduce.cu.
set(LIGER_CUTE_FSLCE_FORWARD_SM90_SOURCE
"${CMAKE_CURRENT_SOURCE_DIR}/src/fused_scaled_linear_cross_entropy/fused_linear_scaled_cross_entropy_forward.cu")
set(LIGER_CUTE_FSLCE_FORWARD_REDUCE_SOURCE
"${CMAKE_CURRENT_SOURCE_DIR}/src/fused_scaled_linear_cross_entropy/forward_reduce.cu")
set(LIGER_CUTE_FSLCE_BACKWARD_SM90_SOURCE
"${CMAKE_CURRENT_SOURCE_DIR}/src/fused_scaled_linear_cross_entropy/fused_linear_scaled_cross_entropy_backward.cu")
set(LIGER_CUTE_FSLCE_BACKWARD_REDUCE_SM90_SOURCE
"${CMAKE_CURRENT_SOURCE_DIR}/src/fused_scaled_linear_cross_entropy/backward_reduce_sm90.cu")
list(REMOVE_ITEM LIGER_CUTE_CORE_SOURCES
"${LIGER_CUTE_FSLCE_FORWARD_SM90_SOURCE}"
"${LIGER_CUTE_FSLCE_FORWARD_REDUCE_SOURCE}"
"${LIGER_CUTE_FSLCE_BACKWARD_SM90_SOURCE}"
"${LIGER_CUTE_FSLCE_BACKWARD_REDUCE_SM90_SOURCE}")
file(GLOB_RECURSE LIGER_CUTE_CORE_HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/include/*.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/*.cuh"
)
list(FILTER LIGER_CUTE_CORE_HEADERS EXCLUDE REGEX "/include/liger_cute/tensor_view\\.h$")
add_library(liger_cute_fslce_forward_sm90 OBJECT
"${LIGER_CUTE_FSLCE_FORWARD_SM90_SOURCE}")
set_target_properties(liger_cute_fslce_forward_sm90 PROPERTIES
CUDA_SEPARABLE_COMPILATION OFF
CXX_VISIBILITY_PRESET hidden
CUDA_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON)
target_compile_definitions(liger_cute_fslce_forward_sm90 PRIVATE
LIGER_CUTE_BUILDING=1)
target_include_directories(liger_cute_fslce_forward_sm90 PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src/moe
${CMAKE_CURRENT_SOURCE_DIR}/src/fused_scaled_linear_cross_entropy
${NCCL_INCLUDE_DIR})
target_link_libraries(liger_cute_fslce_forward_sm90 PRIVATE
CUTLASS::CUTLASS
CUDA::cudart
${NCCL_LIBRARY})
add_library(liger_cute_fslce_forward_reduce OBJECT
"${LIGER_CUTE_FSLCE_FORWARD_REDUCE_SOURCE}")
set_target_properties(liger_cute_fslce_forward_reduce PROPERTIES
CUDA_SEPARABLE_COMPILATION ON
CXX_VISIBILITY_PRESET hidden
CUDA_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON)
target_compile_definitions(liger_cute_fslce_forward_reduce PRIVATE
LIGER_CUTE_BUILDING=1)
target_compile_options(liger_cute_fslce_forward_reduce PRIVATE
$<$<COMPILE_LANGUAGE:CUDA>:-UNVSHMEM_ENABLE_ALL_DEVICE_INLINING>)
target_include_directories(liger_cute_fslce_forward_reduce PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src/moe
${CMAKE_CURRENT_SOURCE_DIR}/src/fused_scaled_linear_cross_entropy)
target_link_libraries(liger_cute_fslce_forward_reduce PRIVATE
CUTLASS::CUTLASS
NVSHMEM::nvshmem_host
NVSHMEM::nvshmem_device
CUDA::cudart
CUDA::cuda_driver)
add_library(liger_cute_fslce_backward_sm90 OBJECT
"${LIGER_CUTE_FSLCE_BACKWARD_SM90_SOURCE}")
set_target_properties(liger_cute_fslce_backward_sm90 PROPERTIES
CUDA_SEPARABLE_COMPILATION OFF
CXX_VISIBILITY_PRESET hidden
CUDA_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON)
target_compile_definitions(liger_cute_fslce_backward_sm90 PRIVATE
LIGER_CUTE_BUILDING=1)
target_include_directories(liger_cute_fslce_backward_sm90 PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src/moe
${CMAKE_CURRENT_SOURCE_DIR}/src/fused_scaled_linear_cross_entropy)
target_link_libraries(liger_cute_fslce_backward_sm90 PRIVATE
CUTLASS::CUTLASS
NVSHMEM::nvshmem_host
CUDA::cudart
CUDA::cuda_driver)
add_library(liger_cute_fslce_backward_reduce_sm90 OBJECT
"${LIGER_CUTE_FSLCE_BACKWARD_REDUCE_SM90_SOURCE}")
set_target_properties(liger_cute_fslce_backward_reduce_sm90 PROPERTIES
CUDA_SEPARABLE_COMPILATION OFF
CXX_VISIBILITY_PRESET hidden
CUDA_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON)
target_compile_definitions(
liger_cute_fslce_backward_reduce_sm90 PRIVATE
LIGER_CUTE_BUILDING=1)
target_include_directories(
liger_cute_fslce_backward_reduce_sm90 PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src/moe
${CMAKE_CURRENT_SOURCE_DIR}/src/fused_scaled_linear_cross_entropy)
target_link_libraries(
liger_cute_fslce_backward_reduce_sm90 PRIVATE
CUTLASS::CUTLASS
NVSHMEM::nvshmem_host
CUDA::cudart
CUDA::cuda_driver)
add_library(liger_cute_kernels SHARED
${LIGER_CUTE_CORE_SOURCES}
${LIGER_CUTE_CORE_HEADERS}
$<TARGET_OBJECTS:liger_cute_fslce_forward_sm90>
$<TARGET_OBJECTS:liger_cute_fslce_forward_reduce>
$<TARGET_OBJECTS:liger_cute_fslce_backward_sm90>
$<TARGET_OBJECTS:liger_cute_fslce_backward_reduce_sm90>
"${CMAKE_CURRENT_SOURCE_DIR}/../../liger_cute_kernels/tvm_ffi_bindings.cpp"
)
@@ -45,13 +148,14 @@ set_target_properties(liger_cute_kernels PROPERTIES
target_compile_definitions(liger_cute_kernels PRIVATE LIGER_CUTE_BUILDING=1)
target_compile_options(liger_cute_kernels PRIVATE ${TVM_FFI_CFLAGS})
target_include_directories(liger_cute_kernels
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/moe
${CMAKE_CURRENT_SOURCE_DIR}/src/fused_scaled_linear_cross_entropy
${NCCL_INCLUDE_DIR}
)
# Deliberately NO torch here — that is the entire point of the split.
@@ -64,6 +168,7 @@ target_link_libraries(liger_cute_kernels PUBLIC
NVSHMEM::nvshmem_device
CUDA::cudart
CUDA::cuda_driver
${NCCL_LIBRARY}
${TVM_FFI_LDFLAGS}
tvm_ffi
)
@@ -0,0 +1,154 @@
#pragma once
// Device-side local reduction algorithms for TpReducePlan backends.
#include <cuda_runtime.h>
#include <cstddef>
#include <cstdint>
#include <type_traits>
#include "liger_cute/detail/nvls.cuh"
#include "liger_cute/detail/tp_reduce.cuh"
namespace liger_cute {
namespace detail {
template <LocalReduceBackend Backend, typename Element>
struct LocalReduceContext;
template <typename Element>
struct LocalReduceContext<LocalReduceBackend::kNvls, Element> {
std::uint64_t* local_ready;
std::uint64_t* multicast_ready;
std::uint64_t* local_complete;
std::uint64_t* multicast_complete;
int rank;
int size;
};
template <typename Element>
struct LocalReduceContext<LocalReduceBackend::kDirectPeer, Element> {
const Element* const* peer_sources;
std::size_t source_offset;
std::uint64_t* local_ready;
std::uint64_t* const* peer_sync;
std::size_t ready_offset;
std::uint64_t* local_complete;
std::size_t complete_offset;
int rank;
int size;
};
__device__ __forceinline__ void direct_peer_barrier_warp(
std::uint64_t* local_signals,
std::uint64_t* const* peer_signals,
std::size_t signal_offset,
int rank,
int size,
std::uint64_t epoch) {
unsigned int lane = nvls_lane_id();
for (int peer = static_cast<int>(lane); peer < size; peer += 32) {
asm volatile(
"st.release.sys.global.u64 [%0], %1;"
:
: "l"(peer_signals[peer] + signal_offset + rank), "l"(epoch)
: "memory");
}
__syncwarp();
for (int peer = static_cast<int>(lane); peer < size; peer += 32) {
std::uint64_t observed = 0;
do {
asm volatile(
"ld.acquire.sys.global.u64 %0, [%1];"
: "=l"(observed)
: "l"(local_signals + peer)
: "memory");
} while (observed != epoch);
}
__syncwarp();
}
template <LocalReduceBackend Backend, typename Element>
__device__ __forceinline__ void local_all_reduce(
const LocalReduceContext<Backend, Element>& context,
Element* destination,
const Element* source,
std::size_t count,
std::uint64_t epoch) {
if constexpr (Backend == LocalReduceBackend::kNvls) {
static_assert(
std::is_same_v<Element, float>,
"NVLS two-shot currently supports FP32 SUM");
nvls_barrier_warp(
context.local_ready,
context.multicast_ready,
context.rank,
context.size,
epoch);
nvls_sum_reduce_warp_twoshot(
destination,
source,
count,
context.rank,
context.size);
nvls_barrier_warp(
context.local_complete,
context.multicast_complete,
context.rank,
context.size,
epoch);
} else {
static_assert(
std::is_same_v<Element, float>,
"direct-peer all-reduce currently supports FP32 SUM");
direct_peer_barrier_warp(
context.local_ready,
context.peer_sync,
context.ready_offset,
context.rank,
context.size,
epoch);
unsigned int lane = nvls_lane_id();
std::size_t vectors = count / 4;
for (std::size_t vector = lane; vector < vectors; vector += 32) {
float4 sum = {0.0f, 0.0f, 0.0f, 0.0f};
for (int peer = 0; peer < context.size; ++peer) {
const auto* peer_source =
reinterpret_cast<const float4*>(
context.peer_sources[peer] +
context.source_offset);
float4 value = peer_source[vector];
sum.x += value.x;
sum.y += value.y;
sum.z += value.z;
sum.w += value.w;
}
reinterpret_cast<float4*>(destination)[vector] = sum;
}
for (std::size_t index = vectors * 4 + lane;
index < count;
index += 32) {
float sum = 0.0f;
for (int peer = 0; peer < context.size; ++peer) {
sum += context.peer_sources[peer][
context.source_offset + index];
}
destination[index] = sum;
}
__syncwarp();
direct_peer_barrier_warp(
context.local_complete,
context.peer_sync,
context.complete_offset,
context.rank,
context.size,
epoch);
}
}
} // namespace detail
} // namespace liger_cute
@@ -0,0 +1,304 @@
// Team-state-free NVLink SHARP primitives.
//
// Callers resolve the multicast pointers once, outside the consuming kernel,
// and pass them as ordinary kernel arguments. The helpers below therefore do
// not include NVSHMEM headers or reference NVSHMEM device state, allowing the
// consuming translation unit to remain non-RDC.
#pragma once
#include <cuda_runtime.h>
#include <cstddef>
#include <cstdint>
namespace liger_cute {
namespace detail {
__device__ __forceinline__ unsigned int nvls_lane_id() {
unsigned int lane = 0;
#if defined(__CUDA_ARCH__)
asm("mov.u32 %0, %%laneid;" : "=r"(lane));
#endif
return lane;
}
// Barrier for one independent NVLS channel.
//
// Each PE multicast-stores its epoch into a distinct rank slot, then waits on
// the local replicas of every slot. Different CTAs may use the same multicast
// mapping concurrently as long as their signal vectors do not overlap.
__device__ __forceinline__ void nvls_barrier_warp(
std::uint64_t* local_signals, std::uint64_t* multicast_signals,
int team_rank, int team_size, std::uint64_t epoch) {
#if defined(__CUDA_ARCH__)
static_assert(__CUDA_ARCH__ >= 900,
"nvls_barrier_warp requires SM90 or newer");
const unsigned int lane = nvls_lane_id();
if (lane == 0) {
asm volatile("multimem.st.release.sys.global.b64 [%0], %1;"
:
: "l"(multicast_signals + team_rank), "l"(epoch)
: "memory");
}
__syncwarp();
for (int rank = static_cast<int>(lane); rank < team_size; rank += 32) {
std::uint64_t observed = 0;
do {
asm volatile("ld.acquire.sys.global.u64 %0, [%1];"
: "=l"(observed)
: "l"(local_signals + rank)
: "memory");
} while (observed != epoch);
}
__syncwarp();
#else
(void)local_signals;
(void)multicast_signals;
(void)team_rank;
(void)team_size;
(void)epoch;
#endif
}
// FP32 SUM all-reduce using the NVLS two-shot algorithm.
//
// Each PE reduces one disjoint slice of the region through the multicast
// source pointer, then multicast-stores that slice to every PE's destination.
// The combination of all corresponding warps therefore produces the complete
// result on every PE.
//
// This primitive intentionally owns no collective state. Concurrent CTAs may
// call it for disjoint, identically prefixed regions through the same TP team's
// multicast mapping. The caller must separately guarantee:
//
// * every PE has published its source region before any PE reads it;
// * one converged full warp per PE calls with warp-uniform arguments;
// * source and destination are not reused until every PE has completed;
// * multicast_source and multicast_dest belong to the same NVLS-capable team.
// * team_size is positive, team_rank is in [0, team_size), and both match the
// multicast mapping's membership;
// * count, rank assignment, and the region prefix are identical across PEs;
// * nonempty regions are at least four-byte aligned and the source/destination
// regions are either exactly in-place or non-overlapping.
//
// The pointers passed here must already include any block/channel prefix.
// A cross-PE completion protocol is still required after this function returns.
// Requires an NVLS-capable SM90-or-newer target.
__device__ __forceinline__ void nvls_sum_reduce_warp_twoshot(
float* multicast_dest, const float* multicast_source, std::size_t count,
int team_rank, int team_size) {
#if defined(__CUDA_ARCH__)
static_assert(__CUDA_ARCH__ >= 900,
"nvls_sum_reduce_warp_twoshot requires SM90 or newer");
if (count == 0) return;
const unsigned int lane = nvls_lane_id();
const std::size_t elements_per_pe =
count / static_cast<std::size_t>(team_size);
const std::size_t rank_offset =
elements_per_pe * static_cast<std::size_t>(team_rank);
const std::size_t local_count =
elements_per_pe +
(team_rank == team_size - 1
? count % static_cast<std::size_t>(team_size)
: 0);
const float* source = multicast_source + rank_offset;
float* dest = multicast_dest + rank_offset;
std::size_t remaining = local_count;
__syncwarp();
asm volatile("" ::: "memory");
if ((reinterpret_cast<std::uintptr_t>(source) & 0xF) == 0 &&
(reinterpret_cast<std::uintptr_t>(dest) & 0xF) == 0) {
const std::size_t vectors = remaining / 4;
const auto* source_vectors = reinterpret_cast<const int4*>(source);
auto* dest_vectors = reinterpret_cast<int4*>(dest);
for (std::size_t index = static_cast<std::size_t>(lane); index < vectors;
index += 32) {
std::uint32_t value[4];
asm volatile(
"multimem.ld_reduce.relaxed.sys.global.add.v4.f32 "
"{%0, %1, %2, %3}, [%4];"
: "=r"(value[0]), "=r"(value[1]), "=r"(value[2]), "=r"(value[3])
: "l"(source_vectors + index)
: "memory");
asm volatile(
"multimem.st.relaxed.sys.global.v4.f32 "
"[%0], {%1, %2, %3, %4};"
:
: "l"(dest_vectors + index), "r"(value[0]), "r"(value[1]),
"r"(value[2]), "r"(value[3])
: "memory");
}
const std::size_t consumed = vectors * 4;
source += consumed;
dest += consumed;
remaining -= consumed;
}
if ((reinterpret_cast<std::uintptr_t>(source) & 0x7) == 0 &&
(reinterpret_cast<std::uintptr_t>(dest) & 0x7) == 0) {
const std::size_t vectors = remaining / 2;
const auto* source_vectors =
reinterpret_cast<const std::uint64_t*>(source);
auto* dest_vectors = reinterpret_cast<std::uint64_t*>(dest);
for (std::size_t index = static_cast<std::size_t>(lane); index < vectors;
index += 32) {
std::uint32_t value[2];
asm volatile(
"multimem.ld_reduce.relaxed.sys.global.add.v2.f32 "
"{%0, %1}, [%2];"
: "=r"(value[0]), "=r"(value[1])
: "l"(source_vectors + index)
: "memory");
asm volatile(
"multimem.st.relaxed.sys.global.v2.f32 [%0], {%1, %2};"
:
: "l"(dest_vectors + index), "r"(value[0]), "r"(value[1])
: "memory");
}
const std::size_t consumed = vectors * 2;
source += consumed;
dest += consumed;
remaining -= consumed;
}
for (std::size_t index = static_cast<std::size_t>(lane); index < remaining;
index += 32) {
std::uint32_t value;
asm volatile("multimem.ld_reduce.relaxed.sys.global.add.f32 %0, [%1];"
: "=r"(value)
: "l"(source + index)
: "memory");
asm volatile("multimem.st.relaxed.sys.global.f32 [%0], %1;"
:
: "l"(dest + index), "r"(value)
: "memory");
}
asm volatile("fence.acq_rel.sys;" ::: "memory");
__syncwarp();
#else
(void)multicast_dest;
(void)multicast_source;
(void)count;
(void)team_rank;
(void)team_size;
#endif
}
__device__ __forceinline__ std::size_t nvls_rank_count(
std::size_t count, int rank, int size) {
std::size_t per_rank = count / static_cast<std::size_t>(size);
return per_rank +
(rank == size - 1 ? count % static_cast<std::size_t>(size) : 0);
}
__device__ __forceinline__ std::size_t nvls_rank_offset(
std::size_t count, int rank, int size) {
return count / static_cast<std::size_t>(size) *
static_cast<std::size_t>(rank);
}
__device__ __forceinline__ void nvls_sum_reduce_scatter_warp(
float* local_dest, const float* multicast_source, std::size_t count,
int team_rank, int team_size) {
#if defined(__CUDA_ARCH__)
const unsigned int lane = nvls_lane_id();
const std::size_t rank_offset =
nvls_rank_offset(count, team_rank, team_size);
const std::size_t local_count =
nvls_rank_count(count, team_rank, team_size);
const float* source = multicast_source + rank_offset;
const std::size_t vectors = local_count / 4;
for (std::size_t index = lane; index < vectors; index += 32) {
std::uint32_t value[4];
asm volatile(
"multimem.ld_reduce.relaxed.sys.global.add.v4.f32 "
"{%0, %1, %2, %3}, [%4];"
: "=r"(value[0]), "=r"(value[1]), "=r"(value[2]), "=r"(value[3])
: "l"(reinterpret_cast<const int4*>(source) + index)
: "memory");
asm volatile(
"st.global.v4.b32 [%0], {%1, %2, %3, %4};"
:
: "l"(reinterpret_cast<int4*>(local_dest) + index), "r"(value[0]),
"r"(value[1]), "r"(value[2]), "r"(value[3])
: "memory");
}
for (std::size_t index = vectors * 4 + lane; index < local_count;
index += 32) {
std::uint32_t value;
asm volatile(
"multimem.ld_reduce.relaxed.sys.global.add.f32 %0, [%1];"
: "=r"(value)
: "l"(source + index)
: "memory");
asm volatile(
"st.global.b32 [%0], %1;"
:
: "l"(local_dest + index), "r"(value)
: "memory");
}
asm volatile("fence.acq_rel.sys;" ::: "memory");
__syncwarp();
#else
(void)local_dest;
(void)multicast_source;
(void)count;
(void)team_rank;
(void)team_size;
#endif
}
__device__ __forceinline__ void nvls_allgather_warp(
float* multicast_dest, const float* local_source, std::size_t count,
int team_rank, int team_size) {
#if defined(__CUDA_ARCH__)
const unsigned int lane = nvls_lane_id();
const std::size_t rank_offset =
nvls_rank_offset(count, team_rank, team_size);
const std::size_t local_count =
nvls_rank_count(count, team_rank, team_size);
float* destination = multicast_dest + rank_offset;
const std::size_t vectors = local_count / 4;
for (std::size_t index = lane; index < vectors; index += 32) {
int4 value = reinterpret_cast<const int4*>(local_source)[index];
asm volatile(
"multimem.st.relaxed.sys.global.v4.f32 "
"[%0], {%1, %2, %3, %4};"
:
: "l"(reinterpret_cast<int4*>(destination) + index), "r"(value.x),
"r"(value.y), "r"(value.z), "r"(value.w)
: "memory");
}
for (std::size_t index = vectors * 4 + lane; index < local_count;
index += 32) {
std::uint32_t value =
reinterpret_cast<const std::uint32_t*>(local_source)[index];
asm volatile(
"multimem.st.relaxed.sys.global.f32 [%0], %1;"
:
: "l"(destination + index), "r"(value)
: "memory");
}
asm volatile("fence.acq_rel.sys;" ::: "memory");
__syncwarp();
#else
(void)multicast_dest;
(void)local_source;
(void)count;
(void)team_rank;
(void)team_size;
#endif
}
} // namespace detail
} // namespace liger_cute
@@ -0,0 +1,111 @@
#pragma once
#include <cuda_runtime.h>
#include <cstddef>
#include <cstdint>
namespace liger_cute {
namespace detail {
inline constexpr int kMaxTpReduceTeamSize = 512;
enum class LocalReduceBackend : std::uint8_t {
kNvls,
kDirectPeer,
};
// Minimal device view for the local NVLS reduce-scatter/all-gather path.
struct NvlsReduceView {
float* multicast_partial;
float* multicast_reduced;
std::uint64_t* multicast_sync;
float* reduced_shard;
int rank;
int size;
};
// Minimal device view for the non-NVLS direct-peer all-reduce path.
struct DirectPeerReduceView {
const float* const* peer_partial;
std::uint64_t* const* peer_sync;
int available;
int rank;
int size;
};
// Host-only description of the optional follow-up remote reduction.
struct RemoteReduceView {
float* reduced_shard;
float* inbox;
std::uint64_t* ready;
std::uint64_t* consumed;
int rank;
int size;
int peer_world;
bool enabled() const {
return peer_world >= 0;
}
};
// Host dispatch plan. Only the selected backend-specific view is passed to a
// kernel; RequiresRemote remains a compile-time launcher parameter.
struct TpReducePlan {
LocalReduceBackend backend;
NvlsReduceView nvls;
DirectPeerReduceView direct;
RemoteReduceView remote;
int team_size;
};
struct TpReduceTopology {
int team_size;
int local_size;
};
struct TpReduceBuffers {
float* partial;
float* reduced;
float* reduced_shard;
float* remote_inbox;
std::uint64_t* remote_signals;
std::uint64_t* sync;
std::size_t sync_bytes;
float** peer_partial_storage;
std::uint64_t** peer_sync_storage;
};
#if defined(__CUDACC__)
__device__ __forceinline__ void publish_local_reduce_source() {
asm volatile("fence.proxy.async.global;" ::: "memory");
__threadfence_system();
}
#endif
// Collective across parent_team. Resolves NVLS and direct-peer mappings once
// and selects the local backend. A two-host remote stage is represented
// separately in TpReducePlan::remote.
void configure_tp_reduce(
std::int64_t parent_team,
const TpReduceBuffers& buffers);
TpReduceTopology query_tp_reduce_topology(std::int64_t parent_team);
TpReducePlan tp_reduce_plan();
void begin_tp_reduce(
const std::uint64_t* launch_epoch,
cudaStream_t stream);
void end_tp_reduce(cudaStream_t stream);
void reset_tp_reduce();
// Separate RDC follow-up stage for the packed shard produced by the local
// reduction. This is intentionally outside the hot non-RDC WGMMA kernel.
void launch_remote_reduce(
const RemoteReduceView& remote,
const std::uint64_t* launch_epoch,
std::size_t count,
cudaStream_t stream);
} // namespace detail
} // namespace liger_cute
@@ -0,0 +1,84 @@
#include "remote_all_reduce.cuh"
#include <nvshmem.h>
#include <nvshmemx.h>
#include "liger_cute/check.h"
namespace liger_cute {
namespace detail {
namespace {
__global__ void remote_put(
float* inbox,
const float* source,
std::size_t count,
int peer) {
nvshmemx_float_put_block(inbox, source, count, peer);
}
__global__ void remote_handshake(
std::uint64_t* signal,
const std::uint64_t* launch_epoch,
std::uint64_t epoch_suffix,
int peer) {
std::uint64_t epoch = *launch_epoch | epoch_suffix;
nvshmemx_signal_op(
signal, epoch, NVSHMEM_SIGNAL_SET, peer);
nvshmem_quiet();
nvshmem_signal_wait_until(signal, NVSHMEM_CMP_EQ, epoch);
}
__global__ void remote_sum(
float* destination,
const float* source,
const float* inbox,
std::size_t count) {
for (std::size_t index =
blockIdx.x * blockDim.x + threadIdx.x;
index < count;
index += blockDim.x * gridDim.x) {
destination[index] = source[index] + inbox[index];
}
}
} // namespace
void launch_remote_pair_all_reduce(
float* destination,
float* inbox,
const float* source,
std::size_t count,
std::uint64_t* ready,
std::uint64_t* consumed,
const std::uint64_t* launch_epoch,
std::uint64_t epoch_suffix,
int peer,
cudaStream_t stream) {
LIGER_CHECK(
destination != nullptr && inbox != nullptr && source != nullptr &&
ready != nullptr && consumed != nullptr &&
launch_epoch != nullptr,
"remote all-reduce buffers must be non-null");
LIGER_CHECK(count > 0, "remote all-reduce count must be positive");
LIGER_CHECK(peer >= 0, "remote all-reduce peer must be non-negative");
remote_put<<<1, 256, 0, stream>>>(inbox, source, count, peer);
nvshmemx_quiet_on_stream(stream);
remote_handshake<<<1, 1, 0, stream>>>(
ready, launch_epoch, epoch_suffix, peer);
int blocks = static_cast<int>(
(count + 255) / 256 < 1024 ? (count + 255) / 256 : 1024);
remote_sum<<<blocks, 256, 0, stream>>>(
destination, source, inbox, count);
remote_handshake<<<1, 1, 0, stream>>>(
consumed, launch_epoch, epoch_suffix, peer);
cudaError_t error = cudaGetLastError();
LIGER_CHECK(
error == cudaSuccess,
"remote pair all-reduce launch failed: ",
cudaGetErrorString(error));
}
} // namespace detail
} // namespace liger_cute
@@ -0,0 +1,26 @@
#pragma once
// Internal implementation detail behind launch_remote_reduce().
#include <cuda_runtime.h>
#include <cstddef>
#include <cstdint>
namespace liger_cute {
namespace detail {
void launch_remote_pair_all_reduce(
float* destination,
float* inbox,
const float* source,
std::size_t count,
std::uint64_t* ready,
std::uint64_t* consumed,
const std::uint64_t* launch_epoch,
std::uint64_t epoch_suffix,
int peer,
cudaStream_t stream);
} // namespace detail
} // namespace liger_cute
@@ -0,0 +1,357 @@
#include "liger_cute/detail/tp_reduce.cuh"
#include <cuda_runtime.h>
#include <nvshmem.h>
#include <nvshmemx.h>
#include <cstddef>
#include <cstdint>
#include <vector>
#include "remote_all_reduce.cuh"
#include "liger_cute/check.h"
namespace liger_cute {
namespace detail {
namespace {
struct RawNvlsMapping {
float* multicast_partial;
float* multicast_reduced;
std::uint64_t* multicast_sync;
float* node_multicast_partial;
float* node_multicast_reduced;
std::uint64_t* node_multicast_sync;
int team_rank;
int team_size;
int node_rank;
int node_size;
int remote_rank;
int remote_size;
};
RawNvlsMapping g_raw = {};
TpReducePlan g_plan = {};
TpReduceBuffers g_buffers = {};
bool g_configured = false;
std::int64_t g_parent_team = 0;
void check_cuda(cudaError_t error, const char* what) {
LIGER_CHECK(
error == cudaSuccess,
"tensor-parallel reduction: ",
what,
" failed: ",
cudaGetErrorString(error));
}
__global__ void query_nvls_mapping(
nvshmem_team_t team,
float* partial,
float* reduced,
std::uint64_t* sync,
RawNvlsMapping* output) {
if (blockIdx.x != 0 || threadIdx.x != 0) return;
output->multicast_partial =
static_cast<float*>(nvshmemx_mc_ptr(team, partial));
output->multicast_reduced =
static_cast<float*>(nvshmemx_mc_ptr(team, reduced));
output->multicast_sync =
static_cast<std::uint64_t*>(nvshmemx_mc_ptr(team, sync));
output->node_multicast_partial =
static_cast<float*>(
nvshmemx_mc_ptr(NVSHMEMX_TEAM_NODE, partial));
output->node_multicast_reduced =
static_cast<float*>(
nvshmemx_mc_ptr(NVSHMEMX_TEAM_NODE, reduced));
output->node_multicast_sync =
static_cast<std::uint64_t*>(
nvshmemx_mc_ptr(NVSHMEMX_TEAM_NODE, sync));
output->team_rank = nvshmem_team_my_pe(team);
output->team_size = nvshmem_team_n_pes(team);
output->node_rank = nvshmem_team_my_pe(NVSHMEMX_TEAM_NODE);
output->node_size = nvshmem_team_n_pes(NVSHMEMX_TEAM_NODE);
output->remote_rank =
nvshmem_team_my_pe(NVSHMEMX_TEAM_SAME_MYPE_NODE);
output->remote_size =
nvshmem_team_n_pes(NVSHMEMX_TEAM_SAME_MYPE_NODE);
}
__global__ void advance_launch_epoch(std::uint64_t* launch_epoch) {
if (blockIdx.x == 0 && threadIdx.x == 0) {
*launch_epoch += std::uint64_t{1} << 32;
}
}
bool same_buffers(const TpReduceBuffers& lhs, const TpReduceBuffers& rhs) {
return lhs.partial == rhs.partial &&
lhs.reduced == rhs.reduced &&
lhs.reduced_shard == rhs.reduced_shard &&
lhs.remote_inbox == rhs.remote_inbox &&
lhs.remote_signals == rhs.remote_signals &&
lhs.sync == rhs.sync &&
lhs.sync_bytes == rhs.sync_bytes &&
lhs.peer_partial_storage == rhs.peer_partial_storage &&
lhs.peer_sync_storage == rhs.peer_sync_storage;
}
} // namespace
TpReduceTopology query_tp_reduce_topology(std::int64_t parent_team) {
nvshmem_team_t team = static_cast<nvshmem_team_t>(parent_team);
int team_size = nvshmem_team_n_pes(team);
int node_size = nvshmem_team_n_pes(NVSHMEMX_TEAM_NODE);
LIGER_CHECK(
team_size >= 1 && team_size <= kMaxTpReduceTeamSize,
"invalid tensor-parallel reduction team size ",
team_size);
LIGER_CHECK(node_size >= 1, "invalid NVSHMEM node-team size");
return {
team_size,
team_size < node_size ? team_size : node_size,
};
}
void configure_tp_reduce(
std::int64_t parent_team,
const TpReduceBuffers& buffers) {
LIGER_CHECK(
buffers.partial != nullptr && buffers.reduced != nullptr &&
buffers.reduced_shard != nullptr &&
buffers.remote_inbox != nullptr &&
buffers.remote_signals != nullptr &&
buffers.sync != nullptr,
"tensor-parallel reduction buffers must be non-null");
LIGER_CHECK(
buffers.sync_bytes > 0,
"tensor-parallel reduction signal storage must be non-empty");
LIGER_CHECK(
buffers.peer_partial_storage != nullptr &&
buffers.peer_sync_storage != nullptr,
"tensor-parallel direct-peer mapping storage must be non-null");
if (g_configured) {
LIGER_CHECK(
parent_team == g_parent_team &&
same_buffers(buffers, g_buffers),
"tensor-parallel reduction mapping is immutable once configured");
return;
}
nvshmem_team_t team = static_cast<nvshmem_team_t>(parent_team);
int team_size = query_tp_reduce_topology(parent_team).team_size;
check_cuda(
cudaMemset(buffers.sync, 0, buffers.sync_bytes),
"cudaMemset(reduction signals)");
check_cuda(
cudaMemset(
buffers.remote_signals,
0,
2 * sizeof(std::uint64_t)),
"cudaMemset(remote reduction signals)");
RawNvlsMapping* device_mapping = nullptr;
check_cuda(
cudaMalloc(&device_mapping, sizeof(RawNvlsMapping)),
"cudaMalloc(reduction mapping)");
query_nvls_mapping<<<1, 1>>>(
team,
buffers.partial,
buffers.reduced,
buffers.sync,
device_mapping);
check_cuda(cudaGetLastError(), "query_nvls_mapping launch");
check_cuda(
cudaMemcpy(
&g_raw,
device_mapping,
sizeof(RawNvlsMapping),
cudaMemcpyDeviceToHost),
"cudaMemcpy(reduction mapping)");
check_cuda(cudaFree(device_mapping), "cudaFree(reduction mapping)");
LIGER_CHECK(
g_raw.team_rank >= 0 &&
g_raw.team_rank < g_raw.team_size &&
g_raw.team_size == team_size,
"inconsistent tensor-parallel reduction team metadata");
std::vector<float*> peer_partial(team_size);
std::vector<std::uint64_t*> peer_sync(team_size);
std::vector<unsigned char> world_members(
static_cast<std::size_t>(nvshmem_n_pes()), 0);
bool direct_available = true;
bool parent_covers_world = team_size == nvshmem_n_pes();
int my_world_pe = nvshmem_my_pe();
for (int rank = 0; rank < team_size; ++rank) {
int world_pe = nvshmem_team_translate_pe(
team, rank, NVSHMEM_TEAM_WORLD);
LIGER_CHECK(
world_pe >= 0,
"failed to translate tensor-parallel rank ",
rank,
" to NVSHMEM_TEAM_WORLD");
if (world_pe >= static_cast<int>(world_members.size()) ||
world_members[world_pe] != 0) {
parent_covers_world = false;
} else {
world_members[world_pe] = 1;
}
peer_partial[rank] = world_pe == my_world_pe
? buffers.partial
: static_cast<float*>(
nvshmem_ptr(buffers.partial, world_pe));
peer_sync[rank] = world_pe == my_world_pe
? buffers.sync
: static_cast<std::uint64_t*>(
nvshmem_ptr(buffers.sync, world_pe));
direct_available =
direct_available && peer_partial[rank] != nullptr &&
peer_sync[rank] != nullptr;
}
check_cuda(
cudaMemcpy(
buffers.peer_partial_storage,
peer_partial.data(),
peer_partial.size() * sizeof(float*),
cudaMemcpyHostToDevice),
"cudaMemcpy(direct-peer partial pointers)");
check_cuda(
cudaMemcpy(
buffers.peer_sync_storage,
peer_sync.data(),
peer_sync.size() * sizeof(std::uint64_t*),
cudaMemcpyHostToDevice),
"cudaMemcpy(direct-peer signal pointers)");
bool nvls_available =
team_size == 1 ||
(g_raw.multicast_partial != nullptr &&
g_raw.multicast_reduced != nullptr &&
g_raw.multicast_sync != nullptr);
bool remote_available =
!nvls_available && parent_covers_world &&
g_raw.node_size > 1 &&
g_raw.remote_size == 2 &&
g_raw.node_size * g_raw.remote_size == team_size &&
g_raw.node_multicast_partial != nullptr &&
g_raw.node_multicast_reduced != nullptr &&
g_raw.node_multicast_sync != nullptr;
g_plan = {};
g_plan.remote.peer_world = -1;
g_plan.team_size = g_raw.team_size;
g_plan.direct = {
buffers.peer_partial_storage,
buffers.peer_sync_storage,
static_cast<int>(direct_available),
g_raw.team_rank,
g_raw.team_size};
if (nvls_available) {
g_plan.backend = LocalReduceBackend::kNvls;
g_plan.nvls = {
team_size == 1 ? buffers.partial : g_raw.multicast_partial,
team_size == 1 ? buffers.reduced : g_raw.multicast_reduced,
team_size == 1 ? buffers.sync : g_raw.multicast_sync,
buffers.reduced_shard,
g_raw.team_rank,
g_raw.team_size};
} else if (remote_available) {
g_plan.backend = LocalReduceBackend::kNvls;
g_plan.nvls = {
g_raw.node_multicast_partial,
g_raw.node_multicast_reduced,
g_raw.node_multicast_sync,
buffers.reduced_shard,
g_raw.node_rank,
g_raw.node_size};
int peer_rank = 1 - g_raw.remote_rank;
int peer_world = nvshmem_team_translate_pe(
NVSHMEMX_TEAM_SAME_MYPE_NODE,
peer_rank,
NVSHMEM_TEAM_WORLD);
LIGER_CHECK(
peer_world >= 0,
"failed to translate remote reduction peer to world rank");
g_plan.remote = {
buffers.reduced_shard,
buffers.remote_inbox,
buffers.remote_signals,
buffers.remote_signals + 1,
g_raw.remote_rank,
g_raw.remote_size,
peer_world};
} else {
g_plan.backend = LocalReduceBackend::kDirectPeer;
}
int barrier_status = nvshmem_barrier(team);
LIGER_CHECK(
barrier_status == 0,
"tensor-parallel reduction setup barrier failed with status ",
barrier_status);
g_parent_team = parent_team;
g_buffers = buffers;
g_configured = true;
}
TpReducePlan tp_reduce_plan() {
LIGER_CHECK(
g_configured,
"configure_tp_reduce() must be called before requesting a plan");
return g_plan;
}
void begin_tp_reduce(
const std::uint64_t* launch_epoch,
cudaStream_t stream) {
LIGER_CHECK(g_configured, "tensor-parallel reduction is not configured");
LIGER_CHECK(launch_epoch != nullptr, "launch epoch is null");
advance_launch_epoch<<<1, 1, 0, stream>>>(
const_cast<std::uint64_t*>(launch_epoch));
check_cuda(cudaGetLastError(), "advance_launch_epoch launch");
}
void end_tp_reduce(cudaStream_t stream) {
LIGER_CHECK(g_configured, "tensor-parallel reduction is not configured");
if (g_plan.team_size > 1) {
nvshmemx_barrier_on_stream(
static_cast<nvshmem_team_t>(g_parent_team), stream);
}
}
void launch_remote_reduce(
const RemoteReduceView& remote,
const std::uint64_t* launch_epoch,
std::size_t count,
cudaStream_t stream) {
LIGER_CHECK(remote.enabled(), "remote reduction is not configured");
LIGER_CHECK(
remote.size == 2 && remote.rank >= 0 && remote.rank < remote.size,
"invalid remote reduction topology");
launch_remote_pair_all_reduce(
remote.reduced_shard,
remote.inbox,
remote.reduced_shard,
count,
remote.ready,
remote.consumed,
launch_epoch,
0x30000000u,
remote.peer_world,
stream);
}
void reset_tp_reduce() {
g_raw = {};
g_plan = {};
g_plan.remote.peer_world = -1;
g_buffers = {};
g_configured = false;
g_parent_team = 0;
}
} // namespace detail
} // namespace liger_cute
@@ -0,0 +1,47 @@
#pragma once
#include <cmath>
#include <cuda_runtime.h>
namespace liger {
namespace fused_scaled_linear_cross_entropy {
__host__ __device__ inline float scaled_cross_entropy_dlogit(
float scaled_logit,
float lse,
bool is_target,
float nll_scale,
float temperature,
float expected_scaled_logit = 0.0f,
float entropy_scale = 0.0f,
bool ignored = false) {
if (ignored) return 0.0f;
float probability = expf(scaled_logit - lse);
float gradient = nll_scale * (probability - (is_target ? 1.0f : 0.0f));
if (entropy_scale != 0.0f) {
gradient += entropy_scale * probability *
(expected_scaled_logit - scaled_logit);
}
return gradient / temperature;
}
struct BackwardWave {
int first_m_tile;
int num_m_tiles;
};
template <typename GemmConfig>
__host__ __device__ constexpr BackwardWave make_backward_wave(
int wave, int total_m_tiles) {
int first_m_tile = wave * GemmConfig::kMTilesPerWave;
int remaining = total_m_tiles - first_m_tile;
int num_m_tiles = remaining < GemmConfig::kMTilesPerWave
? remaining
: GemmConfig::kMTilesPerWave;
return {first_m_tile, num_m_tiles > 0 ? num_m_tiles : 0};
}
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,381 @@
#pragma once
// ═══════════════════════════════════════════════════════════════════════════
// SM90 fused scaled linear cross entropy — backward GEMM contract.
//
// Structural port of
// src/liger_kernel/ops/cutedsl/ops/_fused_scaled_cross_entropy_backward_fused_sm90.py
// (the CuTe-DSL `_FusedBackwardSM90` one-kernel backward). Constant names below
// map onto the reference module constants:
//
// THREADS_PER_CTA 384 tile_m 128 tile_n 256 tile_k 64 stages 3
// m_tiles_per_wave 8 wave_rows 1024
// dz_store_stages 1 -> 2 dw_store_stages 4 -> 2
// epi_registers 88 -> 104 mma_registers 208 -> 200
//
// The kernel keeps the reference's three fused phases and its wave loop:
//
// phase dZ Z = X @ W_local^T, softmax/entropy epilogue -> dZ wave buffer
// grid barrier
// phase dX dX_partial = dZ @ W_local, epilogue -> CTA-owned symmetric
// staging, the same CTA's warps 1..2 all-reduce and store dX
// grid barrier
// phase dW dW_local = dZ^T @ X, TMA store (wave 0) / reduce-add (wave > 0)
// grid barrier (unless this was the last wave)
//
// ── Deliberate deltas from the single-GPU reference ───────────────────────
// * Warp roles are fixed by the tensor-parallel contract: warp 0 is the TMA
// producer for all three phases, warps 1..2 reduce that same CTA's dX groups,
// warp 3 is reserved and idle on every path, warps 4..11 are the two WGMMA
// consumer warp groups. The reference gave warp group 0 (warps 0..3) the dZ
// softmax epilogue; here warps 1..3 are unavailable, so the epilogue runs in
// the consumer warp groups straight out of the FP32 WGMMA accumulators. The
// reference's FP16 logit staging round-trip is reproduced in register
// (float -> half -> float) so the numerics are unchanged.
// * CLUSTER_M is 1, so the dX B operand is not multicast. The resident grid is
// launched through nvshmemx_collective_launch() for cross-PE co-scheduling;
// that API takes no cluster launch attribute. The reference's cluster-pair
// M multicast is therefore not expressible and is dropped.
// * The epilogue store tile is N64 rather than the reference's N32 for dZ/dW.
// A BF16 N64 row is exactly 128 B, which is what the SW128 store staging
// used by dZ and dW requires. dW already used N64 in the reference.
// * dX stays FP32 from WGMMA through a compact symmetric staging ring and the
// NVLS SUM. The communication warps convert once to X.dtype while
// scattering the final reduced result into the caller's row-major tensor
// (BF16 for this SM90 specialization). dW is untouched and stays rank-local.
//
// This header is deliberately CuTe-free: launch geometry, the wave/tile
// schedule, the warp plan and the launcher declarations only, so torch-free
// consumers can include the umbrella header without CUTLASS. The executable
// producer/consumer/mainloop lives in backward_gemm_mainloop_sm90.cuh and is
// instantiated by fused_linear_scaled_cross_entropy_backward.cu.
// ═══════════════════════════════════════════════════════════════════════════
#include <cuda_runtime.h>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include "backward.cuh"
#include "config.cuh"
#include "dx_reduce.cuh"
namespace liger {
namespace fused_scaled_linear_cross_entropy {
inline constexpr float kBackwardLog2E = 1.4426950408889634f;
inline constexpr int kBackwardMaxSmemBytes = 227 * 1024;
// exp2 with the reference's fastmath=True lowering (llvm.nvvm.ex2.approx.ftz).
__host__ __device__ inline float backward_exp2_sm90(float value) {
#if defined(__CUDA_ARCH__)
float result;
asm("ex2.approx.ftz.f32 %0, %1;" : "=f"(result) : "f"(value));
return result;
#else
return exp2f(value);
#endif
}
template <int Compute = 90>
struct BackwardGemmConfigSm90 {
static_assert(Compute == 90,
"SM90 fused scaled linear cross entropy requires Compute=90");
static constexpr int kCompute = Compute;
static constexpr int kTileM = 128;
static constexpr int kTileK = 64;
static constexpr int kDzTileN = 256;
static constexpr bool kDzNoTmaStore = false;
static constexpr int kDzLogicalTileN = kDzTileN;
static constexpr int kDxTileN = 256;
static constexpr int kDwTileN = 256;
static constexpr int kDwTileM = kTileM;
static constexpr int kDwLogicalTileN = kDwTileN;
static constexpr int kMainloopStages = 3;
static constexpr int kMTilesPerWave = 8;
static constexpr int kWaveRows = kTileM * kMTilesPerWave;
static constexpr int kDzStoreTileN = 64;
static constexpr int kDzStoreStages = 2;
static constexpr int kDwStoreTileN = 64;
static constexpr int kDwStoreStages = 2;
static constexpr int kClusterM = 1;
static constexpr int kNumThreads =
fused_scaled_linear_cross_entropy::kNumThreads;
static constexpr int kNumMmaWarpGroups = 2;
static constexpr int kWarpsPerWarpGroup = 4;
static constexpr int kWarpGroupSize = 128;
static constexpr int kConsumerThreads =
kNumMmaWarpGroups * kWarpGroupSize;
static constexpr int kFirstConsumerThread = kWarpGroupSize;
// The dZ workspace K extent must be a whole number of K tiles so the dX /
// dW mainloops never see a ragged K tail on an operand they own.
static constexpr int kVocabAlign = kTileK;
// setmaxnreg budget. Warps 0..3 share one warp group, so the communication
// warps take the producer allocation. The selected host-waved combined
// dX+dW specialization uses 56/224; separate phase-specialized kernels
// retain their budgets below.
static constexpr int kProducerRegisters = 56;
static constexpr int kMmaRegisters = 224;
static constexpr int kDzProducerRegisters = 24;
static constexpr int kDzMmaRegisters = 240;
static constexpr int kDxDwProducerRegisters = 72;
static constexpr int kDxDwMmaRegisters = 216;
static constexpr int kUsableRegisterBudget = 64512;
// Hardware named barriers. 0 is __syncthreads().
static constexpr int kDzEpilogueBarrierId = 4; // consumer warp groups
static constexpr int kDxEpilogueBarrierId = 5; // consumer warp groups
static constexpr int kDwStoreBarrierId = 6; // consumer warp groups
// Hands the aliased A/B and store-staging arenas from the dX phase to the
// dW phase. Deliberately NOT __syncthreads(): warps 1..2 are still
// draining the dX reduction ring and must not be pulled into it, and warp
// 3 is reserved. Only the producer warp and the two consumer warp groups
// touch the aliased storage, so only they take part.
static constexpr int kComputeBarrierId = 8;
static constexpr int kComputeBarrierThreads =
(1 + kNumMmaWarpGroups * kWarpsPerWarpGroup) * kWarpSize;
static_assert(kNumThreads == (kNumMmaWarpGroups + 1) * kWarpGroupSize);
static_assert(kWaveRows % kTileM == 0);
static_assert(kDzTileN % kDzStoreTileN == 0);
static_assert(kComputeBarrierThreads == 288,
"the compute barrier covers warp 0 and warps 4..11");
static_assert(kComputeBarrierThreads % kWarpSize == 0);
static_assert(kComputeBarrierThreads < kNumThreads,
"the compute barrier must exclude the communication and reserved "
"warps, otherwise it degenerates into __syncthreads()");
static_assert(kDwTileN % kDwStoreTileN == 0);
static_assert(backward_warp_role(0) == BackwardWarpRole::kProducer);
static_assert(backward_warp_role(1) == BackwardWarpRole::kDxCommunication);
static_assert(backward_warp_role(2) == BackwardWarpRole::kDxCommunication);
static_assert(backward_warp_role(3) == BackwardWarpRole::kReserved);
static_assert(backward_warp_role(4) == BackwardWarpRole::kConsumer);
static_assert(backward_warp_role(11) == BackwardWarpRole::kConsumer);
__host__ __device__ static constexpr int register_total() {
return kWarpGroupSize * kProducerRegisters +
kNumMmaWarpGroups * kWarpGroupSize * kMmaRegisters;
}
static_assert(register_total() <= kUsableRegisterBudget,
"SM90 backward register budget exceeds the usable per-CTA budget");
static_assert(
kWarpGroupSize * kDzProducerRegisters +
kNumMmaWarpGroups * kWarpGroupSize * kDzMmaRegisters <=
kUsableRegisterBudget,
"SM90 dZ register budget exceeds the usable per-CTA budget");
static_assert(
kWarpGroupSize * kDxDwProducerRegisters +
kNumMmaWarpGroups * kWarpGroupSize * kDxDwMmaRegisters <=
kUsableRegisterBudget,
"SM90 dX+dW register budget exceeds the usable per-CTA budget");
};
// The reference's per-phase geometry, kept as named contracts so a future
// SM100 port can specialise them without touching the mainloop.
template <int Compute = 90>
struct DzGemmContractSm90 {
using Config = BackwardGemmConfigSm90<Compute>;
static constexpr int kCompute = Compute;
static constexpr int kTileM = Config::kTileM;
static constexpr int kTileN = Config::kDzTileN;
static constexpr int kTileK = Config::kTileK;
static constexpr int kStoreTileN = Config::kDzStoreTileN;
static constexpr int kStoreStages = Config::kDzStoreStages;
};
template <int Compute = 90>
struct DxGemmContractSm90 {
using Config = BackwardGemmConfigSm90<Compute>;
static constexpr int kCompute = Compute;
static constexpr int kTileM = Config::kTileM;
static constexpr int kTileN = Config::kDxTileN;
static constexpr int kTileK = Config::kTileK;
};
template <int Compute = 90>
struct DwGemmContractSm90 {
using Config = BackwardGemmConfigSm90<Compute>;
static constexpr int kCompute = Compute;
static constexpr int kTileM = Config::kTileM;
static constexpr int kTileN = Config::kDwTileN;
static constexpr int kTileK = Config::kTileK;
static constexpr int kStoreTileN = Config::kDwStoreTileN;
static constexpr int kStoreStages = Config::kDwStoreStages;
};
template <int Compute = 90>
struct BackwardWarpPlanSm90 {
using Config = BackwardGemmConfigSm90<Compute>;
static constexpr int kCompute = Compute;
static constexpr int kProducerWarp = 0;
static constexpr int kFirstDxCommWarp = 1;
static constexpr int kLastDxCommWarp = 2;
static constexpr int kReservedWarp = 3;
static constexpr int kFirstConsumerWarp = 4;
static constexpr int kLastConsumerWarp = 11;
// The consumer warp that issues every TMA store (the reference's `issuer`).
static constexpr int kIssuerWarp = kFirstConsumerWarp;
static_assert(Config::kNumThreads == kNumThreads);
static_assert(kLastConsumerWarp + 1 == kNumWarps);
static_assert(backward_warp_role(kReservedWarp) ==
BackwardWarpRole::kReserved);
};
// ───────────────────────────────────────────────────────────────────────────
// Problem description
// ───────────────────────────────────────────────────────────────────────────
// The vocabulary shard is contiguous: local column c is global vocabulary index
// `vocab_start + c`, and targets arrive as *global* int64 indices. `lse` and
// `entropy` are the forward's already-all-reduced per-token outputs, so they
// are ordinary local tensors — nothing symmetric is carried from forward.
template <int Compute = 90>
struct BackwardGemmParamsSm90 {
using Config = BackwardGemmConfigSm90<Compute>;
static constexpr int kCompute = Compute;
const void* x = nullptr; // BF16 [tokens, hidden]
const void* weight = nullptr; // BF16 [local_vocab, hidden]
const std::int64_t* target = nullptr; // int64 [tokens], global indices
const float* grad_output = nullptr; // FP32 [tokens], upstream NLL scale
const float* lse = nullptr; // FP32 [tokens], global LSE
const float* entropy = nullptr; // FP32 [tokens], global entropy
const float* entropy_grad = nullptr; // FP32 [tokens], upstream entropy scale
void* grad_input = nullptr; // X.dtype [tokens, hidden], BF16 on SM90
void* grad_weight = nullptr; // BF16 [local_vocab, hidden], rank local
// Internal scratch, launcher owned (see BackwardSymmetricNames).
void* dz_workspace = nullptr; // BF16 [kWaveRows, padded_local_vocab]
std::size_t dz_workspace_bytes = 0;
int tokens = 0;
int hidden = 0;
int local_vocab = 0;
std::int64_t vocab_start = 0;
std::int64_t ignore_index = -100;
float inverse_temperature = 1.0f;
// Selftest-only skew injection; production callers leave the defaults.
int dx_comm_delay_warp = -1;
int dx_comm_delay_iterations = 0;
};
// ───────────────────────────────────────────────────────────────────────────
// Launch geometry
// ───────────────────────────────────────────────────────────────────────────
template <int Compute = 90>
struct BackwardGemmLaunchSm90 {
using Config = BackwardGemmConfigSm90<Compute>;
static constexpr int kCompute = Compute;
static_assert(Compute == 90,
"SM90 fused scaled linear cross entropy requires Compute=90");
__host__ __device__ static constexpr int num_waves(int tokens) {
return ceil_div(tokens, Config::kWaveRows);
}
// Vocabulary padded to a whole K tile so dX/dW never see a ragged K tail.
__host__ __device__ static constexpr int padded_vocab(int local_vocab) {
return ceil_div(local_vocab, Config::kVocabAlign) * Config::kVocabAlign;
}
__host__ __device__ static constexpr int num_dz_n_tiles(int local_vocab) {
return ceil_div(
padded_vocab(local_vocab), Config::kDzLogicalTileN);
}
__host__ __device__ static constexpr int num_dx_n_tiles(int hidden) {
return ceil_div(hidden, Config::kDxTileN);
}
__host__ __device__ static constexpr int num_dx_k_tiles(int local_vocab) {
return padded_vocab(local_vocab) / Config::kTileK;
}
__host__ __device__ static constexpr int num_dz_k_tiles(int hidden) {
return ceil_div(hidden, Config::kTileK);
}
__host__ __device__ static constexpr int num_dw_m_tiles(int local_vocab) {
return ceil_div(padded_vocab(local_vocab), Config::kDwTileM);
}
__host__ __device__ static constexpr int num_dw_n_tiles(int hidden) {
return ceil_div(hidden, Config::kDwLogicalTileN);
}
__host__ __device__ static constexpr int num_dw_k_tiles() {
return Config::kWaveRows / Config::kTileK;
}
// One wave of dZ, BF16.
__host__ __device__ static std::size_t dz_workspace_bytes(
int local_vocab) {
return static_cast<std::size_t>(Config::kWaveRows) *
static_cast<std::size_t>(padded_vocab(local_vocab)) * 2u;
}
// Number of dX reduction groups produced by the whole launch, for a given
// TilesPerReduce. Waves are always full (`kMTilesPerWave` M tiles) so the
// group count is a pure function of the shape on every PE.
__host__ __device__ static int num_dx_groups(
int tokens, int hidden, int tiles_per_reduce) {
int groups_per_m = ceil_div(num_dx_n_tiles(hidden), tiles_per_reduce);
return num_waves(tokens) * Config::kMTilesPerWave * groups_per_m;
}
};
// ───────────────────────────────────────────────────────────────────────────
// Host launcher
//
// `params.grad_input` is filled with the TP-reduced dX and `params.grad_weight`
// with this rank's dW. Collective: every PE of the configured TP team calls
// this with the
// same shapes and TilesPerReduce.
// ───────────────────────────────────────────────────────────────────────────
template <int Compute = 90>
struct BackwardTpParamsSm90 {
static constexpr int kCompute = Compute;
BackwardGemmParamsSm90<Compute> gemm = {};
// Compile-time knob selected at run time; 1, 2 (default) or 4.
int tiles_per_reduce = 2;
// Retained for FFI compatibility; CTA ownership ignores this value.
int num_comm_channels = 4;
};
__host__ __device__ constexpr int backward_dx_split_k(
int hidden, int local_vocab) {
// Split-K2 requires an even number of K64 vocabulary tiles. Ragged odd
// tile counts use split-K1 so the final tile is never dropped.
int k_tiles = (local_vocab + 63) / 64;
return hidden == 2048 && k_tiles % 2 == 0 ? 2 : 1;
}
template <bool ReturnEntropy, int Compute = 90>
void fused_linear_scaled_cross_entropy_backward(
const BackwardTpParamsSm90<Compute>& params,
cudaStream_t stream);
extern template void fused_linear_scaled_cross_entropy_backward<false, 90>(
const BackwardTpParamsSm90<90>&,
cudaStream_t);
extern template void fused_linear_scaled_cross_entropy_backward<true, 90>(
const BackwardTpParamsSm90<90>&,
cudaStream_t);
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,753 @@
#include "backward_reduce_sm90.cuh"
#include <cuda_bf16.h>
#include <cuda_runtime.h>
#include <device/nvshmemx_collective_launch_apis.h>
#include <cstddef>
#include <cstdint>
#include "backward_gemm_mainloop_sm90.cuh"
#include "liger_cute/check.h"
#include "liger_cute/detail/local_reduce.cuh"
#include "workspace.cuh"
namespace liger {
namespace fused_scaled_linear_cross_entropy {
namespace {
using HostConfig = BackwardGemmConfigSm90<90>;
using HostLaunch = BackwardGemmLaunchSm90<90>;
using HostTraits = BackwardGemmTraitsSm90<90>;
using HostElement = HostTraits::Element;
void check_cuda(cudaError_t error, const char* what) {
LIGER_CHECK(
error == cudaSuccess,
"fused_scaled_linear_cross_entropy backward reduction: ",
what,
" failed: ",
cudaGetErrorString(error));
}
template <class Layout, class Element>
auto make_row_major_load(const Element* ptr, int64_t rows, int64_t cols) {
return make_tma_copy(
SM90_TMA_LOAD{},
backward_row_major_tensor(ptr, rows, cols),
Layout{});
}
template <class Layout, class Element>
auto make_row_major_store(Element* ptr, int64_t rows, int64_t cols) {
return make_tma_copy(
SM90_TMA_STORE{},
backward_row_major_tensor(ptr, rows, cols),
Layout{});
}
template <class Layout, class Element>
auto make_row_major_reduce_add(Element* ptr, int64_t rows, int64_t cols) {
return make_tma_copy(
SM90_TMA_REDUCE_ADD{},
backward_row_major_tensor(ptr, rows, cols),
Layout{});
}
template <class Layout, class Element>
auto make_col_major_load(
const Element* ptr, int64_t rows, int64_t cols, int64_t leading) {
return make_tma_copy(
SM90_TMA_LOAD{},
backward_col_major_tensor(ptr, rows, cols, leading),
Layout{});
}
template <class CommConfig, int Compute>
__device__ void direct_peer_communicate(
BackwardDxDwSmemSm90<Compute>& smem,
const BackwardGemmParamsSm90<Compute>& params,
const DxReduceWorkspace<float>& comm,
const liger_cute::detail::DirectPeerReduceView& mapping,
int num_n_tiles,
int cta,
int num_ctas,
int wave,
int groups_per_wave) {
using Traits = BackwardGemmTraitsSm90<Compute>;
using Config = typename Traits::Config;
int comm_warp =
static_cast<int>(threadIdx.x) / Traits::kWarpSize -
CommConfig::kFirstCommWarp;
int lane = static_cast<int>(threadIdx.x) % Traits::kWarpSize;
auto* grad_input = static_cast<__nv_bfloat16*>(params.grad_input);
int next_index = 0;
for (int unit = cta; unit < groups_per_wave; unit += num_ctas) {
int group_id = wave * groups_per_wave + unit;
DxTileGroup group =
dx_unit_to_group<CommConfig>(unit, num_n_tiles, group_id);
DxCtaGroupSlot slot =
dx_cta_group_slot<CommConfig>(next_index);
std::uint64_t ready_epoch =
static_cast<std::uint64_t>(group_id) + 1;
if (lane == 0) {
dx_ready_wait_acquire(
&smem.dx_ready_epoch[comm_warp][slot.stage],
ready_epoch);
}
__syncwarp();
std::size_t message_rows =
static_cast<std::size_t>(group.num_tiles) * Traits::kTileM;
std::size_t first_row =
message_rows * static_cast<std::size_t>(comm_warp) /
kDxCommWarpsPerChannel;
std::size_t last_row =
message_rows * static_cast<std::size_t>(comm_warp + 1) /
kDxCommWarpsPerChannel;
std::size_t segment_elements =
(last_row - first_row) * Traits::kTileN;
std::size_t segment_begin = first_row * Traits::kTileN;
std::size_t base =
dx_slot_offset<CommConfig>(cta, slot.comm_warp, slot.stage);
std::size_t ready_offset = dx_sync_offset<CommConfig>(
cta,
comm_warp,
slot.stage,
kDxReadyPhase,
mapping.size);
std::size_t complete_offset = dx_sync_offset<CommConfig>(
cta,
comm_warp,
slot.stage,
kDxCompletePhase,
mapping.size);
std::uint64_t epoch =
dx_epoch_base(comm) |
(static_cast<std::uint64_t>(wave + 1) << 16) |
static_cast<std::uint64_t>(slot.pass + 1);
constexpr std::size_t kValuesPerVector = 4;
std::size_t segment_vectors =
segment_elements / kValuesPerVector;
float* local_reduced = comm.reduced + base + segment_begin;
liger_cute::detail::LocalReduceContext<
liger_cute::detail::LocalReduceBackend::kDirectPeer,
float> context{
mapping.peer_partial,
base + segment_begin,
comm.sync + ready_offset,
mapping.peer_sync,
ready_offset,
comm.sync + complete_offset,
complete_offset,
mapping.rank,
mapping.size};
liger_cute::detail::local_all_reduce<
liger_cute::detail::LocalReduceBackend::kDirectPeer>(
context,
local_reduced,
comm.partial + base + segment_begin,
segment_elements,
epoch);
constexpr std::size_t kVectorsPerTile =
Traits::kTileM * Traits::kTileN / kValuesPerVector;
std::size_t segment_vector_begin =
segment_begin / kValuesPerVector;
for (std::size_t segment_vector = lane;
segment_vector < segment_vectors;
segment_vector += Traits::kWarpSize) {
std::size_t message_vector =
segment_vector_begin + segment_vector;
int tile = static_cast<int>(
message_vector / kVectorsPerTile);
int vector_in_tile = static_cast<int>(
message_vector % kVectorsPerTile);
int row = vector_in_tile / (Traits::kTileN / 4);
int column =
(vector_in_tile % (Traits::kTileN / 4)) * 4;
int output_row = wave * Config::kWaveRows +
group.m_tile * Traits::kTileM + row;
int output_column =
(group.first_n_tile + tile) * Traits::kTileN + column;
if (output_row < params.tokens &&
output_column < params.hidden) {
float4 values =
reinterpret_cast<const float4*>(
local_reduced)[segment_vector];
typename DxPhaseSm90<
CommConfig, Compute>::Bfloat16x4 packed{
__floats2bfloat162_rn(values.x, values.y),
__floats2bfloat162_rn(values.z, values.w)};
auto* output = reinterpret_cast<
typename DxPhaseSm90<
CommConfig, Compute>::Bfloat16x4*>(
grad_input +
static_cast<std::size_t>(output_row) *
params.hidden +
output_column);
*output = packed;
}
}
__syncwarp();
if (lane == 0) {
cute::arrive_barrier(
smem.dx_consumed_barrier[comm_warp][slot.stage]);
}
++next_index;
}
}
template <
bool RunDx,
bool RunDw,
bool ReturnEntropy,
int Compute,
class CommConfig,
class Bundle>
__global__ __launch_bounds__(BackwardGemmTraitsSm90<Compute>::kNumThreads, 1)
void backward_dx_dw_direct_peer_wave_kernel_sm90(
__grid_constant__ const Bundle tma,
__grid_constant__ const BackwardGemmParamsSm90<Compute> params,
__grid_constant__ const DxReduceWorkspace<float> comm,
__grid_constant__ const liger_cute::detail::DirectPeerReduceView mapping,
int wave) {
using Traits = BackwardGemmTraitsSm90<Compute>;
using Config = typename Traits::Config;
using Launch = BackwardGemmLaunchSm90<Compute>;
using Smem = BackwardDxDwSmemSm90<Compute>;
using PipelineTraits = typename Traits::PipelineTraits;
using ClusterShape = typename Traits::ClusterShape;
using Registers = sm90::WarpSpecializedRegistersSm90<
Config::kDxDwProducerRegisters, Config::kDxDwMmaRegisters, Compute>;
extern __shared__ char raw_smem[];
Smem& smem = *reinterpret_cast<Smem*>(raw_smem);
int warp_group = cutlass::canonical_warp_group_idx();
int warp = cutlass::canonical_warp_idx_sync();
PipelineTraits::init_barriers(
smem.mainloop_pipeline,
Traits::kTmaTransBytes,
Traits::kConsumerThreads,
ClusterShape{});
if (threadIdx.x == 0) {
CUTE_UNROLL
for (int comm_warp = 0;
comm_warp < kDxCommWarpsPerChannel;
++comm_warp) {
CUTE_UNROLL
for (int stage = 0; stage < kDxRingStages; ++stage) {
smem.dx_ready_epoch[comm_warp][stage] = 0;
cute::initialize_barrier(
smem.dx_consumed_barrier[comm_warp][stage], 1);
}
}
}
sm90::cluster_pipeline_init_sm90<Compute>();
int num_ctas = static_cast<int>(gridDim.x);
int cta = static_cast<int>(blockIdx.x);
if (warp_group == 0) {
Registers::producer();
if (warp == 0) {
int padded_vocab = Launch::padded_vocab(params.local_vocab);
auto state = PipelineTraits::producer_start_state();
auto pipe = PipelineTraits::make_producer(
smem.mainloop_pipeline,
Traits::kTmaTransBytes,
Traits::kConsumerThreads,
threadIdx.x == 0,
ClusterShape{});
if constexpr (RunDx) {
int dx_n_tiles = Launch::num_dx_n_tiles(params.hidden);
int dx_k_tiles =
Launch::num_dx_k_tiles(params.local_vocab);
int dx_groups_per_wave = Config::kMTilesPerWave *
dx_groups_per_m_tile<CommConfig>(dx_n_tiles);
DxPhaseSm90<CommConfig, Compute>::
template produce<ReturnEntropy>(
pipe,
state,
smem,
tma.dz_load,
tma.wt,
params,
padded_vocab,
dx_n_tiles,
dx_k_tiles,
dx_groups_per_wave,
cta,
num_ctas);
}
if constexpr (RunDx && RunDw) {
backward_compute_barrier_sm90<Compute>();
}
if constexpr (RunDw) {
int dw_n_tiles = Launch::num_dw_n_tiles(params.hidden);
constexpr int kDwKTiles = Launch::num_dw_k_tiles();
int dw_total =
Launch::num_dw_m_tiles(params.local_vocab) * dw_n_tiles;
int dw_tiles = dw_total > cta
? ceil_div(dw_total - cta, num_ctas)
: 0;
DwPhaseSm90<Compute>::template produce<ReturnEntropy>(
pipe,
state,
smem,
tma.dzt,
tma.xt,
params,
padded_vocab,
wave,
dw_n_tiles,
kDwKTiles,
dw_tiles,
cta,
num_ctas);
}
} else if (
warp >= CommConfig::kFirstCommWarp &&
warp <= CommConfig::kLastCommWarp) {
if constexpr (RunDx) {
int dx_n_tiles = Launch::num_dx_n_tiles(params.hidden);
int dx_groups_per_wave = Config::kMTilesPerWave *
dx_groups_per_m_tile<CommConfig>(dx_n_tiles);
direct_peer_communicate<CommConfig, Compute>(
smem,
params,
comm,
mapping,
dx_n_tiles,
cta,
num_ctas,
wave,
dx_groups_per_wave);
}
}
} else {
Registers::consumer();
typename Traits::PipelineState read_state;
typename Traits::PipelineState release_state;
auto pipe = PipelineTraits::make_consumer(
smem.mainloop_pipeline,
Traits::kTmaTransBytes,
Traits::kConsumerThreads,
ClusterShape{});
if constexpr (RunDx) {
int dx_n_tiles = Launch::num_dx_n_tiles(params.hidden);
int dx_k_tiles =
Launch::num_dx_k_tiles(params.local_vocab);
int dx_groups_per_wave = Config::kMTilesPerWave *
dx_groups_per_m_tile<CommConfig>(dx_n_tiles);
int next_index = 0;
DxPhaseSm90<CommConfig, Compute>::
template consume<ReturnEntropy>(
pipe,
read_state,
release_state,
smem,
comm,
dx_n_tiles,
dx_k_tiles,
wave,
dx_groups_per_wave,
cta,
num_ctas,
next_index);
}
if constexpr (RunDx && RunDw) {
backward_compute_barrier_sm90<Compute>();
}
if constexpr (RunDw) {
int dw_n_tiles = Launch::num_dw_n_tiles(params.hidden);
constexpr int kDwKTiles = Launch::num_dw_k_tiles();
int dw_total =
Launch::num_dw_m_tiles(params.local_vocab) * dw_n_tiles;
int dw_tiles = dw_total > cta
? ceil_div(dw_total - cta, num_ctas)
: 0;
DwPhaseSm90<Compute>::template consume<ReturnEntropy>(
pipe,
read_state,
release_state,
smem,
tma.dw_store,
tma.dw_add,
params,
wave,
dw_n_tiles,
kDwKTiles,
dw_tiles,
cta,
num_ctas);
}
}
__syncthreads();
sm90::cluster_exit_sm90<Compute>();
}
template <int Compute>
__global__ void cluster_local_allgather_scatter_kernel(
__grid_constant__ const BackwardGemmParamsSm90<Compute> params,
__grid_constant__ const DxReduceWorkspace<float> comm,
__grid_constant__ const liger_cute::detail::NvlsReduceView mapping,
int grid_ctas,
int wave,
int tiles_per_wave,
int num_n_tiles) {
using Traits = BackwardGemmTraitsSm90<Compute>;
using Config = typename Traits::Config;
using CommConfig = DxCommConfig<Config, kDxRingStages, 1, Compute>;
int unit = static_cast<int>(blockIdx.x);
if (unit >= tiles_per_wave) return;
int comm_warp = static_cast<int>(threadIdx.x) / Traits::kWarpSize;
int lane = static_cast<int>(threadIdx.x) % Traits::kWarpSize;
if (comm_warp >= kDxCommWarpsPerChannel) return;
constexpr std::size_t kTileElements = CommConfig::kTileElements;
constexpr std::size_t kSegmentElements =
kTileElements / kDxCommWarpsPerChannel;
std::size_t packed_tile_elements =
kTileElements / mapping.size;
std::size_t packed_segment_elements =
kSegmentElements / mapping.size;
const float* packed_source = mapping.reduced_shard +
(static_cast<std::size_t>(wave) * tiles_per_wave + unit) *
packed_tile_elements +
static_cast<std::size_t>(comm_warp) * packed_segment_elements;
int cta = unit % grid_ctas;
int next_index = unit / grid_ctas;
DxCtaGroupSlot slot = dx_cta_group_slot<CommConfig>(next_index);
const float* values = packed_source;
if (mapping.size > 1) {
std::size_t ready_offset = dx_sync_offset<CommConfig>(
cta,
comm_warp,
slot.stage,
kDxReadyPhase,
mapping.size);
std::size_t complete_offset = dx_sync_offset<CommConfig>(
cta,
comm_warp,
slot.stage,
kDxCompletePhase,
mapping.size);
std::uint64_t epoch =
dx_epoch_base(comm) | 0x20000000u |
(static_cast<std::uint64_t>(wave) << 16) |
static_cast<std::uint64_t>(slot.pass + 1);
liger_cute::detail::nvls_barrier_warp(
comm.sync + ready_offset,
mapping.multicast_sync + ready_offset,
mapping.rank,
mapping.size,
epoch);
std::size_t full_tile_offset =
(static_cast<std::size_t>(wave) * tiles_per_wave + unit) *
kTileElements;
std::size_t segment_begin =
static_cast<std::size_t>(comm_warp) * kSegmentElements;
liger_cute::detail::nvls_allgather_warp(
mapping.multicast_reduced +
full_tile_offset + segment_begin,
packed_source,
kSegmentElements,
mapping.rank,
mapping.size);
liger_cute::detail::nvls_barrier_warp(
comm.sync + complete_offset,
mapping.multicast_sync + complete_offset,
mapping.rank,
mapping.size,
epoch);
values = comm.reduced + full_tile_offset + segment_begin;
}
auto* grad_input = static_cast<__nv_bfloat16*>(params.grad_input);
std::size_t segment_begin =
static_cast<std::size_t>(comm_warp) * kSegmentElements;
int m_tile = unit / num_n_tiles;
int n_tile = unit % num_n_tiles;
constexpr std::size_t kSegmentVectors = kSegmentElements / 4;
for (std::size_t vector = lane;
vector < kSegmentVectors;
vector += Traits::kWarpSize) {
std::size_t tile_vector = segment_begin / 4 + vector;
int row = static_cast<int>(
tile_vector / (Traits::kTileN / 4));
int column = static_cast<int>(
(tile_vector % (Traits::kTileN / 4)) * 4);
int output_row = wave * Config::kWaveRows +
m_tile * Traits::kTileM + row;
int output_column = n_tile * Traits::kTileN + column;
if (output_row < params.tokens &&
output_column < params.hidden) {
float4 value =
reinterpret_cast<const float4*>(values)[vector];
typename DxPhaseSm90<CommConfig, Compute>::Bfloat16x4 packed{
__floats2bfloat162_rn(value.x, value.y),
__floats2bfloat162_rn(value.z, value.w)};
auto* output = reinterpret_cast<
typename DxPhaseSm90<
CommConfig, Compute>::Bfloat16x4*>(
grad_input +
static_cast<std::size_t>(output_row) *
params.hidden +
output_column);
*output = packed;
}
}
}
template <bool RequiresRemote>
void finalize_cluster_typed(
const BackwardGemmParamsSm90<90>& params,
const DxReduceWorkspace<float>& comm,
const liger_cute::detail::NvlsReduceView& mapping,
const liger_cute::detail::RemoteReduceView& remote,
int grid,
int num_waves,
cudaStream_t stream) {
using CommConfig = DxCommConfig<HostConfig, kDxRingStages, 1, 90>;
int num_n_tiles = HostLaunch::num_dx_n_tiles(params.hidden);
int tiles_per_wave = HostConfig::kMTilesPerWave * num_n_tiles;
std::size_t full_elements =
static_cast<std::size_t>(num_waves) *
static_cast<std::size_t>(tiles_per_wave) *
CommConfig::kTileElements;
std::size_t packed_elements =
full_elements / static_cast<std::size_t>(mapping.size);
LIGER_CHECK(
full_elements * sizeof(float) <=
backward_dx_configured_durable_bytes(),
"fused_scaled_linear_cross_entropy backward: cluster dX durable "
"workspace capacity exceeded");
if constexpr (RequiresRemote) {
liger_cute::detail::launch_remote_reduce(
remote,
comm.launch_epoch,
packed_elements,
stream);
}
auto* kernel = &cluster_local_allgather_scatter_kernel<90>;
for (int wave = 0; wave < num_waves; ++wave) {
void* args[] = {
const_cast<BackwardGemmParamsSm90<90>*>(&params),
const_cast<DxReduceWorkspace<float>*>(&comm),
const_cast<liger_cute::detail::NvlsReduceView*>(&mapping),
&grid,
&wave,
&tiles_per_wave,
&num_n_tiles};
int status = nvshmemx_collective_launch(
reinterpret_cast<const void*>(kernel),
dim3(static_cast<unsigned>(tiles_per_wave), 1, 1),
dim3(kDxCommWarpsPerChannel * kWarpSize, 1, 1),
args,
0,
stream);
LIGER_CHECK(
status == 0,
"fused_scaled_linear_cross_entropy backward: cluster local "
"all-gather/scatter launch failed with status ",
status);
check_cuda(
cudaGetLastError(),
"cluster_local_allgather_scatter_kernel launch");
}
}
template <
int TilesPerReduce,
bool ReturnEntropy,
bool RunDx = true,
bool RunDw = true>
void launch_typed(
const BackwardGemmParamsSm90<90>& params,
const DxReduceWorkspace<float>& comm,
const liger_cute::detail::DirectPeerReduceView& mapping,
int grid,
cudaStream_t stream,
int wave) {
using CommConfig =
DxCommConfig<HostConfig, kDxRingStages, TilesPerReduce, 90>;
using Smem = BackwardDxDwSmemSm90<90>;
auto* w = static_cast<const HostElement*>(params.weight);
auto* dz = static_cast<HostElement*>(params.dz_workspace);
auto* dw = static_cast<HostElement*>(params.grad_weight);
int padded_vocab = HostLaunch::padded_vocab(params.local_vocab);
auto tma_dz_load =
make_row_major_load<typename HostTraits::SmemAKMajor::Single>(
dz, HostConfig::kWaveRows, padded_vocab);
auto tma_wt = make_col_major_load<
typename HostTraits::SmemBMnMajor::Single>(
w, params.hidden, params.local_vocab, params.hidden);
auto tma_dzt = make_col_major_load<
typename HostTraits::SmemAMnMajor::Single>(
dz, padded_vocab, HostConfig::kWaveRows, padded_vocab);
auto tma_xt = make_col_major_load<
typename HostTraits::SmemBMnMajor::Single>(
static_cast<const HostElement*>(params.x),
params.hidden,
params.tokens,
params.hidden);
auto tma_dw_store =
make_row_major_store<typename HostTraits::DwSmemStore1::Single>(
dw, params.local_vocab, params.hidden);
auto tma_dw_add =
make_row_major_reduce_add<typename HostTraits::DwSmemStore1::Single>(
dw, params.local_vocab, params.hidden);
using Bundle = BackwardDxDwTmaBundleSm90<
decltype(tma_dz_load),
decltype(tma_wt),
decltype(tma_dzt),
decltype(tma_xt),
decltype(tma_dw_store),
decltype(tma_dw_add)>;
Bundle bundle{
tma_dz_load,
tma_wt,
tma_dzt,
tma_xt,
tma_dw_store,
tma_dw_add};
constexpr int kSmemBytes = static_cast<int>(sizeof(Smem));
static_assert(
2 * kSmemBytes > kBackwardMaxSmemBytes,
"the direct-peer grid assumes at most one resident CTA per SM");
auto* kernel =
&backward_dx_dw_direct_peer_wave_kernel_sm90<
RunDx,
RunDw,
ReturnEntropy,
90,
CommConfig,
Bundle>;
check_cuda(
cudaFuncSetAttribute(
kernel,
cudaFuncAttributeMaxDynamicSharedMemorySize,
kSmemBytes),
"cudaFuncSetAttribute(MaxDynamicSharedMemorySize)");
void* args[] = {
&bundle,
const_cast<BackwardGemmParamsSm90<90>*>(&params),
const_cast<DxReduceWorkspace<float>*>(&comm),
const_cast<liger_cute::detail::DirectPeerReduceView*>(&mapping),
&wave};
int status = nvshmemx_collective_launch(
reinterpret_cast<const void*>(kernel),
dim3(static_cast<unsigned>(grid), 1, 1),
dim3(HostConfig::kNumThreads, 1, 1),
args,
static_cast<std::size_t>(kSmemBytes),
stream);
LIGER_CHECK(
status == 0,
"fused_scaled_linear_cross_entropy backward: "
"non-NVLS collective launch failed with status ",
status);
check_cuda(
cudaGetLastError(),
"backward_dx_dw_direct_peer_wave_kernel_sm90 launch");
}
} // namespace
void launch_backward_dx_dw_direct_peer_wave_sm90(
bool return_entropy,
int tiles_per_reduce,
const BackwardGemmParamsSm90<90>& params,
const DxReduceWorkspace<float>& comm,
const liger_cute::detail::DirectPeerReduceView& mapping,
int grid,
cudaStream_t stream,
int wave) {
LIGER_CHECK(
mapping.size > 1,
"fused_scaled_linear_cross_entropy backward: invalid direct-peer team "
"metadata");
LIGER_CHECK(
mapping.available != 0,
"fused_scaled_linear_cross_entropy backward: NVLS is unavailable and "
"one or more tensor-parallel peers do not have a direct symmetric "
"mapping");
if (return_entropy) {
switch (tiles_per_reduce) {
case 1:
launch_typed<1, true>(
params, comm, mapping, grid, stream, wave);
return;
case 2:
launch_typed<2, true>(
params, comm, mapping, grid, stream, wave);
return;
default:
launch_typed<4, true>(
params, comm, mapping, grid, stream, wave);
return;
}
}
switch (tiles_per_reduce) {
case 1:
launch_typed<1, false>(
params, comm, mapping, grid, stream, wave);
return;
case 2:
launch_typed<2, false>(
params, comm, mapping, grid, stream, wave);
return;
default:
launch_typed<4, false>(
params, comm, mapping, grid, stream, wave);
return;
}
}
template <bool RequiresRemote>
void finalize_backward_dx_cluster_sm90(
const BackwardGemmParamsSm90<90>& params,
const DxReduceWorkspace<float>& comm,
const liger_cute::detail::NvlsReduceView& mapping,
const liger_cute::detail::RemoteReduceView& remote,
int grid,
int num_waves,
cudaStream_t stream) {
finalize_cluster_typed<RequiresRemote>(
params, comm, mapping, remote, grid, num_waves, stream);
}
template void finalize_backward_dx_cluster_sm90<false>(
const BackwardGemmParamsSm90<90>& params,
const DxReduceWorkspace<float>& comm,
const liger_cute::detail::NvlsReduceView& mapping,
const liger_cute::detail::RemoteReduceView& remote,
int grid,
int num_waves,
cudaStream_t stream);
template void finalize_backward_dx_cluster_sm90<true>(
const BackwardGemmParamsSm90<90>& params,
const DxReduceWorkspace<float>& comm,
const liger_cute::detail::NvlsReduceView& mapping,
const liger_cute::detail::RemoteReduceView& remote,
int grid,
int num_waves,
cudaStream_t stream);
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,51 @@
#pragma once
// FSLCE-specific adapters around the reusable tensor-parallel reduction API.
#include <cuda_runtime.h>
#include "backward_gemm_sm90.cuh"
#include "liger_cute/detail/tp_reduce.cuh"
namespace liger {
namespace fused_scaled_linear_cross_entropy {
void launch_backward_dx_dw_direct_peer_wave_sm90(
bool return_entropy,
int tiles_per_reduce,
const BackwardGemmParamsSm90<90>& params,
const DxReduceWorkspace<float>& comm,
const liger_cute::detail::DirectPeerReduceView& mapping,
int grid,
cudaStream_t stream,
int wave);
template <bool RequiresRemote>
void finalize_backward_dx_cluster_sm90(
const BackwardGemmParamsSm90<90>& params,
const DxReduceWorkspace<float>& comm,
const liger_cute::detail::NvlsReduceView& mapping,
const liger_cute::detail::RemoteReduceView& remote,
int grid,
int num_waves,
cudaStream_t stream);
extern template void finalize_backward_dx_cluster_sm90<false>(
const BackwardGemmParamsSm90<90>& params,
const DxReduceWorkspace<float>& comm,
const liger_cute::detail::NvlsReduceView& mapping,
const liger_cute::detail::RemoteReduceView& remote,
int grid,
int num_waves,
cudaStream_t stream);
extern template void finalize_backward_dx_cluster_sm90<true>(
const BackwardGemmParamsSm90<90>& params,
const DxReduceWorkspace<float>& comm,
const liger_cute::detail::NvlsReduceView& mapping,
const liger_cute::detail::RemoteReduceView& remote,
int grid,
int num_waves,
cudaStream_t stream);
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,701 @@
// ═══════════════════════════════════════════════════════════════════════════
// Internal self test for the tensor-parallel SM90 backward path.
//
// Compiled out unless LIGER_FSLCE_BACKWARD_SM90_SELFTEST is defined, so the
// core library's recursive .cu glob picks this file up as an empty translation
// unit. Build it with the production CMake objects so the non-RDC cluster/dW
// kernels and the isolated RDC fused fallback retain their production flags.
// NVSHMEM_REMOTE_TRANSPORT=none NVSHMEM_DISABLE_NCCL=1 \
// NVSHMEM_SYMMETRIC_SIZE=3G \
// nvshmrun -n 4 ./backward_selftest
//
// Diagnostics (environment variables, all optional):
// FSLCE_POISON=s|z|sz wipe the symmetric dX staging (s) and/or the dZ wave
// workspace (z) between the two attempts of a case, so
// a case that only passes on the second attempt cannot
// hide behind buffers it never wrote.
// FSLCE_CHANNELS=<n> override the ignored legacy channel argument.
// FSLCE_DELAY_WARP=0|1 inject delay into one CTA-local comm warp.
// FSLCE_DELAY_ITERS=n number of 64-cycle sleeps before each owned chunk.
// FSLCE_ATTEMPTS=n repeat every case n times to expose ordering races.
//
// It runs at any PE count, including one:
// * dX is the FP32 TP *sum* over shards followed by one conversion to
// X.dtype (BF16 in this specialization), so every PE must end up with the same
// globally correct dX. The CPU reference rebuilds the whole vocabulary
// from the shared seed and sums every shard's contribution, so a dropped
// reduction, a wrong team, a mis-sliced tail group or a stale ring slot
// all show up.
// * dW is rank local, so it is checked against this shard's own tiles only.
// TilesPerReduce is swept, including one-tile chunks that alternate whole
// messages between the two comm warps. Legacy channel values are varied to
// verify that they no longer affect CTA ownership.
// ═══════════════════════════════════════════════════════════════════════════
#if defined(LIGER_FSLCE_BACKWARD_SM90_SELFTEST)
#include <arpa/inet.h>
#include <nvshmem.h>
#include <nvshmemx.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cuda_bf16.h>
#include <cuda_fp16.h>
#include <cuda_runtime.h>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include "backward_gemm_sm90.cuh"
#include "buffer_pool.cuh"
#include "workspace.cuh"
namespace {
namespace fslce = liger::fused_scaled_linear_cross_entropy;
using Element = __nv_bfloat16;
int g_pe = 0;
int g_n_pes = 1;
int g_failures = 0;
constexpr int kDefaultAttempts = 2;
constexpr int kMaxTokens = 1280;
constexpr int kMaxHidden = 8448;
constexpr int kMaxLocalVocab = 1024;
constexpr int kMaxTilesPerReduce = 4;
constexpr int kLegacyMaxCommChannels = 4;
void send_all(int fd, const void* data, std::size_t bytes) {
const char* cursor = static_cast<const char*>(data);
while (bytes != 0) {
ssize_t sent = send(fd, cursor, bytes, 0);
if (sent <= 0) std::exit(1);
cursor += sent;
bytes -= static_cast<std::size_t>(sent);
}
}
void recv_all(int fd, void* data, std::size_t bytes) {
char* cursor = static_cast<char*>(data);
while (bytes != 0) {
ssize_t received = recv(fd, cursor, bytes, 0);
if (received <= 0) std::exit(1);
cursor += received;
bytes -= static_cast<std::size_t>(received);
}
}
void exchange_unique_id(
int rank,
int ranks,
const char* master,
int port,
nvshmemx_uniqueid_t* id) {
if (rank == 0) {
int server = socket(AF_INET, SOCK_STREAM, 0);
int reuse = 1;
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
sockaddr_in address = {};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);
address.sin_port = htons(static_cast<std::uint16_t>(port));
if (bind(server, reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0 ||
listen(server, ranks) != 0) {
std::exit(1);
}
for (int remote = 1; remote < ranks; ++remote) {
int peer = accept(server, nullptr, nullptr);
if (peer < 0) std::exit(1);
send_all(peer, id, sizeof(*id));
close(peer);
}
close(server);
return;
}
sockaddr_in address = {};
address.sin_family = AF_INET;
address.sin_port = htons(static_cast<std::uint16_t>(port));
if (inet_pton(AF_INET, master, &address.sin_addr) != 1) std::exit(1);
for (int attempt = 0; attempt < 200; ++attempt) {
int client = socket(AF_INET, SOCK_STREAM, 0);
if (connect(
client,
reinterpret_cast<sockaddr*>(&address),
sizeof(address)) == 0) {
recv_all(client, id, sizeof(*id));
close(client);
return;
}
close(client);
usleep(100000);
}
std::exit(1);
}
#define CUDA_OK(expr) \
do { \
cudaError_t status = (expr); \
if (status != cudaSuccess) { \
std::printf("[pe %d] CUDA error %s at %s:%d: %s\n", \
g_pe, #expr, __FILE__, __LINE__, \
cudaGetErrorString(status)); \
std::exit(1); \
} \
} while (0)
// Deterministic from (seed, index) so every PE reconstructs the same global
// tensors without any communication.
float element_at(std::uint64_t seed, std::size_t index, float scale) {
std::uint64_t state = seed ^ (index * 0x9E3779B97F4A7C15ULL);
state = state * 6364136223846793005ULL + 1442695040888963407ULL;
state ^= state >> 29;
state = state * 0xBF58476D1CE4E5B9ULL;
state ^= state >> 32;
std::uint32_t bits = static_cast<std::uint32_t>(state >> 33);
return (static_cast<float>(bits % 2048) / 1024.0f - 1.0f) * scale;
}
struct Reference {
std::vector<float> dx; // [tokens, hidden], TP-summed
std::vector<float> dw_local; // [local_vocab, hidden], this shard
std::vector<float> lse; // [tokens]
std::vector<float> entropy; // [tokens]
};
// Full-vocabulary CPU reference. Reproduces the kernel's FP16 logit staging
// round so the two agree on the softmax input bit for bit.
Reference reference(
const std::vector<float>& x,
const std::vector<float>& weight_global,
const std::vector<std::int64_t>& target,
const std::vector<float>& grad_output,
const std::vector<float>& entropy_grad,
int tokens,
int hidden,
int global_vocab,
int local_vocab,
int vocab_start,
std::int64_t ignore_index,
float inverse_temperature,
bool return_entropy) {
Reference out;
out.dx.assign(static_cast<std::size_t>(tokens) * hidden, 0.0f);
out.dw_local.assign(
static_cast<std::size_t>(local_vocab) * hidden, 0.0f);
out.lse.assign(tokens, 0.0f);
out.entropy.assign(tokens, 0.0f);
std::vector<float> scaled(global_vocab);
std::vector<float> dz(global_vocab);
for (int m = 0; m < tokens; ++m) {
float row_max = -1.0e38f;
for (int v = 0; v < global_vocab; ++v) {
float dot = 0.0f;
for (int k = 0; k < hidden; ++k) {
dot += x[static_cast<std::size_t>(m) * hidden + k] *
weight_global[static_cast<std::size_t>(v) * hidden + k];
}
// float -> half -> float, matching the reference's FP16 logit
// staging buffer.
scaled[v] = __half2float(__float2half(dot)) * inverse_temperature;
row_max = fmaxf(row_max, scaled[v]);
}
double sum = 0.0;
double weighted = 0.0;
for (int v = 0; v < global_vocab; ++v) {
double e = std::exp(
static_cast<double>(scaled[v]) - static_cast<double>(row_max));
sum += e;
weighted += e * static_cast<double>(scaled[v]);
}
float lse = row_max + static_cast<float>(std::log(sum));
out.lse[m] = lse;
out.entropy[m] = lse - static_cast<float>(weighted / sum);
bool ignored = target[m] == ignore_index;
float scale = ignored ? 0.0f : grad_output[m];
float escale = (ignored || !return_entropy) ? 0.0f : entropy_grad[m];
for (int v = 0; v < global_vocab; ++v) {
float value = 0.0f;
if (scale != 0.0f || escale != 0.0f) {
float probability = std::exp(scaled[v] - lse);
value = probability *
(scale + (lse - out.entropy[m] - scaled[v]) * escale);
}
if (!ignored && v == static_cast<int>(target[m])) value -= scale;
value *= inverse_temperature;
// dZ is materialised in BF16 by the kernel.
dz[v] = __bfloat162float(__float2bfloat16(value));
}
for (int h = 0; h < hidden; ++h) {
float reduced = 0.0f;
for (int pe = 0; pe < global_vocab / local_vocab; ++pe) {
float partial = 0.0f;
int shard_start = pe * local_vocab;
for (int v = 0; v < local_vocab; ++v) {
int global_v = shard_start + v;
partial += dz[global_v] *
weight_global[
static_cast<std::size_t>(global_v) * hidden + h];
}
reduced += partial;
}
out.dx[static_cast<std::size_t>(m) * hidden + h] = reduced;
}
for (int v = 0; v < local_vocab; ++v) {
float value = dz[vocab_start + v];
if (value == 0.0f) continue;
for (int h = 0; h < hidden; ++h) {
out.dw_local[static_cast<std::size_t>(v) * hidden + h] +=
value * x[static_cast<std::size_t>(m) * hidden + h];
}
}
}
return out;
}
struct Comparison {
int bad;
float max_absolute;
float max_relative;
};
Comparison compare(
const char* label,
const std::vector<float>& actual,
const std::vector<float>& expected,
float absolute,
float relative,
int reported) {
Comparison result = {};
for (std::size_t i = 0; i < expected.size(); ++i) {
float error = fabsf(actual[i] - expected[i]);
float relative_error = error / fmaxf(fabsf(expected[i]), 1.0e-6f);
result.max_absolute = fmaxf(result.max_absolute, error);
result.max_relative = fmaxf(result.max_relative, relative_error);
float tolerance = absolute + relative * fabsf(expected[i]);
if (!(error <= tolerance)) {
if (result.bad < reported) {
std::printf(
"[pe %d] FAIL %-4s [%zu]: got %12.6f want %12.6f "
"(tol %.6f)\n",
g_pe, label, i, actual[i], expected[i], tolerance);
}
++result.bad;
}
}
return result;
}
struct CaseShape {
int tokens;
int hidden;
int local_vocab;
float temperature;
std::uint64_t seed;
};
void check_chunk_ownership() {
using TestCommConfig = fslce::DxCommConfig<
fslce::BackwardGemmConfigSm90<90>, fslce::kDxRingStages, 2>;
constexpr int kGroupsPerWave = 136;
constexpr int kNumCtas = 132;
constexpr int kNumWaves = 3;
int seen[kNumCtas] = {};
for (int group = 0; group < kNumWaves * kGroupsPerWave; ++group) {
int unit = group % kGroupsPerWave;
int cta = unit % kNumCtas;
int index = fslce::dx_cta_group_index(
group, kGroupsPerWave, kNumCtas);
fslce::DxCtaGroupSlot slot =
fslce::dx_cta_group_slot<TestCommConfig>(index);
bool valid = index == seen[cta] &&
slot.comm_warp == 0 &&
slot.index_in_warp == index &&
slot.stage == slot.index_in_warp % fslce::kDxRingStages &&
slot.pass == slot.index_in_warp / fslce::kDxRingStages;
if (!valid) {
std::printf(
"[pe %d] invalid dX chunk owner: group=%d cta=%d "
"index=%d expected=%d warp=%d warp_index=%d stage=%d pass=%d\n",
g_pe, group, cta, index, seen[cta], slot.comm_warp,
slot.index_in_warp, slot.stage, slot.pass);
++g_failures;
}
++seen[cta];
}
}
template <bool ReturnEntropy>
void run_case(
const std::string& name,
const CaseShape& shape,
int tiles_per_reduce,
int num_comm_channels) { // NOLINT: mutated by FSLCE_CHANNELS
const char* filter = std::getenv("FSLCE_FILTER");
if (filter != nullptr && name.find(filter) == std::string::npos) return;
// ABI-compatibility diagnostic: this value no longer routes groups.
const char* channel_override = std::getenv("FSLCE_CHANNELS");
if (channel_override != nullptr) {
num_comm_channels = std::atoi(channel_override);
}
const int tokens = shape.tokens;
const int hidden = shape.hidden;
const int local_vocab = shape.local_vocab;
const int global_vocab = local_vocab * g_n_pes;
const int vocab_start = g_pe * local_vocab;
const float scale = 3.0f / std::sqrt(static_cast<float>(hidden));
std::vector<float> host_x(static_cast<std::size_t>(tokens) * hidden);
for (std::size_t i = 0; i < host_x.size(); ++i) {
host_x[i] = __bfloat162float(
__float2bfloat16(element_at(shape.seed, i, scale)));
}
std::vector<float> host_w(
static_cast<std::size_t>(global_vocab) * hidden);
for (std::size_t i = 0; i < host_w.size(); ++i) {
host_w[i] = __bfloat162float(__float2bfloat16(
element_at(shape.seed ^ 0xABCDEF01ULL, i, scale)));
}
std::vector<std::int64_t> host_target(tokens);
std::vector<float> host_grad_output(tokens);
std::vector<float> host_entropy_grad(tokens);
for (int m = 0; m < tokens; ++m) {
if (m % 13 == 5) {
host_target[m] = -100; // ignore_index
} else {
std::uint64_t state = shape.seed ^ (0x5DEECE66DULL * (m + 1));
state = state * 6364136223846793005ULL + 1442695040888963407ULL;
host_target[m] = static_cast<std::int64_t>(
(state >> 17) % static_cast<std::uint64_t>(global_vocab));
}
host_grad_output[m] = 0.5f + 0.5f * element_at(shape.seed ^ 7u, m, 1.0f);
host_entropy_grad[m] = 0.25f * element_at(shape.seed ^ 11u, m, 1.0f);
}
Reference expected = reference(
host_x, host_w, host_target, host_grad_output, host_entropy_grad,
tokens, hidden, global_vocab, local_vocab, vocab_start, -100,
1.0f / shape.temperature, ReturnEntropy);
// Device operands: X is replicated, W is this PE's contiguous shard.
std::vector<Element> device_x_host(host_x.size());
for (std::size_t i = 0; i < host_x.size(); ++i) {
device_x_host[i] = __float2bfloat16(host_x[i]);
}
std::vector<Element> device_w_host(
static_cast<std::size_t>(local_vocab) * hidden);
for (std::size_t i = 0; i < device_w_host.size(); ++i) {
device_w_host[i] = __float2bfloat16(
host_w[static_cast<std::size_t>(vocab_start) * hidden + i]);
}
Element* device_x = nullptr;
Element* device_w = nullptr;
Element* device_dx = nullptr;
Element* device_dw = nullptr;
std::int64_t* device_target = nullptr;
float* device_scalars = nullptr; // grad_output | lse | entropy | e_grad
CUDA_OK(cudaMalloc(&device_x, device_x_host.size() * sizeof(Element)));
CUDA_OK(cudaMalloc(&device_w, device_w_host.size() * sizeof(Element)));
CUDA_OK(cudaMalloc(&device_dx, device_x_host.size() * sizeof(Element)));
CUDA_OK(cudaMalloc(&device_dw, device_w_host.size() * sizeof(Element)));
CUDA_OK(cudaMalloc(&device_target, tokens * sizeof(std::int64_t)));
CUDA_OK(cudaMalloc(&device_scalars, 4 * tokens * sizeof(float)));
CUDA_OK(cudaMemcpy(device_x, device_x_host.data(),
device_x_host.size() * sizeof(Element), cudaMemcpyHostToDevice));
CUDA_OK(cudaMemcpy(device_w, device_w_host.data(),
device_w_host.size() * sizeof(Element), cudaMemcpyHostToDevice));
CUDA_OK(cudaMemcpy(device_target, host_target.data(),
tokens * sizeof(std::int64_t), cudaMemcpyHostToDevice));
CUDA_OK(cudaMemcpy(device_scalars + 0 * tokens, host_grad_output.data(),
tokens * sizeof(float), cudaMemcpyHostToDevice));
CUDA_OK(cudaMemcpy(device_scalars + 1 * tokens, expected.lse.data(),
tokens * sizeof(float), cudaMemcpyHostToDevice));
CUDA_OK(cudaMemcpy(device_scalars + 2 * tokens, expected.entropy.data(),
tokens * sizeof(float), cudaMemcpyHostToDevice));
CUDA_OK(cudaMemcpy(device_scalars + 3 * tokens, host_entropy_grad.data(),
tokens * sizeof(float), cudaMemcpyHostToDevice));
// Poison the outputs so a skipped store is a failure, not a pass.
CUDA_OK(cudaMemset(device_dx, 0x7f,
device_x_host.size() * sizeof(Element)));
CUDA_OK(cudaMemset(device_dw, 0x7f,
device_w_host.size() * sizeof(Element)));
fslce::BackwardScratch scratch =
fslce::reserve_backward_scratch(local_vocab);
// Diagnostic: FSLCE_POISON=s wipes the symmetric dX staging between
// attempts, =z wipes the dZ wave workspace, =sz both. Any case that only
// passes without poisoning is reading a buffer it never wrote.
const char* poison = std::getenv("FSLCE_POISON");
bool poison_staging = poison != nullptr && std::strchr(poison, 's');
bool poison_dz = poison != nullptr && std::strchr(poison, 'z');
fslce::DxReduceWorkspace<float> staging =
fslce::reserve_dx_reduce_workspace(
tiles_per_reduce,
fslce::kDxRingStages,
fslce::backward_dx_resident_cta_capacity());
std::size_t staging_bytes =
fslce::backward_dx_staging_bytes(kMaxTilesPerReduce);
fslce::BackwardTpParamsSm90<90> params;
params.gemm.x = device_x;
params.gemm.weight = device_w;
params.gemm.target = device_target;
params.gemm.grad_output = device_scalars + 0 * tokens;
params.gemm.lse = device_scalars + 1 * tokens;
params.gemm.entropy = device_scalars + 2 * tokens;
params.gemm.entropy_grad = device_scalars + 3 * tokens;
params.gemm.grad_input = device_dx;
params.gemm.grad_weight = device_dw;
params.gemm.dz_workspace = scratch.dz_workspace;
params.gemm.dz_workspace_bytes = scratch.dz_workspace_bytes;
params.gemm.tokens = tokens;
params.gemm.hidden = hidden;
params.gemm.local_vocab = local_vocab;
params.gemm.vocab_start = vocab_start;
params.gemm.ignore_index = -100;
params.gemm.inverse_temperature = 1.0f / shape.temperature;
const char* delay_warp = std::getenv("FSLCE_DELAY_WARP");
const char* delay_iters = std::getenv("FSLCE_DELAY_ITERS");
const char* attempts_env = std::getenv("FSLCE_ATTEMPTS");
int attempts = attempts_env == nullptr
? kDefaultAttempts
: std::atoi(attempts_env);
if (attempts < 1) attempts = 1;
if (delay_warp != nullptr) {
params.gemm.dx_comm_delay_warp = std::atoi(delay_warp);
}
if (delay_iters != nullptr) {
params.gemm.dx_comm_delay_iterations = std::atoi(delay_iters);
}
params.tiles_per_reduce = tiles_per_reduce;
params.num_comm_channels = num_comm_channels;
// Each case is launched twice: a first-launch-only failure is a warm-up
// bug, a failure that moves between repetitions is a race.
cudaEvent_t start;
cudaEvent_t stop;
CUDA_OK(cudaEventCreate(&start));
CUDA_OK(cudaEventCreate(&stop));
for (int attempt = 0; attempt < attempts; ++attempt) {
if (poison_staging) {
CUDA_OK(cudaMemset(staging.partial, 0, staging_bytes));
CUDA_OK(cudaMemset(staging.reduced, 0, staging_bytes));
}
if (poison_dz) {
CUDA_OK(cudaMemset(
scratch.dz_workspace, 0, scratch.dz_workspace_bytes));
}
CUDA_OK(cudaMemset(device_dx, 0x7f,
device_x_host.size() * sizeof(Element)));
CUDA_OK(cudaMemset(device_dw, 0x7f,
device_w_host.size() * sizeof(Element)));
nvshmem_barrier_all();
CUDA_OK(cudaEventRecord(start));
fslce::fused_linear_scaled_cross_entropy_backward<
ReturnEntropy, 90>(params, nullptr);
CUDA_OK(cudaEventRecord(stop));
CUDA_OK(cudaGetLastError());
CUDA_OK(cudaEventSynchronize(stop));
float elapsed_ms = 0.0f;
CUDA_OK(cudaEventElapsedTime(&elapsed_ms, start, stop));
nvshmem_barrier_all();
std::vector<Element> raw_dx(device_x_host.size());
std::vector<Element> raw_dw(device_w_host.size());
CUDA_OK(cudaMemcpy(raw_dx.data(), device_dx,
raw_dx.size() * sizeof(Element), cudaMemcpyDeviceToHost));
CUDA_OK(cudaMemcpy(raw_dw.data(), device_dw,
raw_dw.size() * sizeof(Element), cudaMemcpyDeviceToHost));
std::vector<float> got_dx(raw_dx.size());
std::vector<float> got_dw(raw_dw.size());
for (std::size_t i = 0; i < raw_dx.size(); ++i) {
got_dx[i] = __bfloat162float(raw_dx[i]);
}
for (std::size_t i = 0; i < raw_dw.size(); ++i) {
got_dw[i] = __bfloat162float(raw_dw[i]);
}
Comparison dx = compare(
"dX", got_dx, expected.dx, 6.0e-3f, 4.0e-2f, 4);
Comparison dw = compare(
"dW", got_dw, expected.dw_local, 8.0e-3f, 4.0e-2f, 4);
g_failures += dx.bad + dw.bad;
if (g_pe == 0) {
std::printf(
"%-42s #%d %s (PEs=%d M=%d H=%d Vloc=%d T=%.2f entropy=%d "
"TPR=%d legacy_chan=%d dx_bad=%d dw_bad=%d ms=%.3f "
"dx_max_abs=%.6g dx_max_rel=%.6g "
"dw_max_abs=%.6g dw_max_rel=%.6g)\n",
name.c_str(), attempt,
(dx.bad + dw.bad) == 0 ? "PASS" : "FAIL",
g_n_pes, tokens, hidden, local_vocab, shape.temperature,
static_cast<int>(ReturnEntropy), tiles_per_reduce,
num_comm_channels, dx.bad, dw.bad, elapsed_ms,
dx.max_absolute, dx.max_relative,
dw.max_absolute, dw.max_relative);
}
}
CUDA_OK(cudaEventDestroy(start));
CUDA_OK(cudaEventDestroy(stop));
CUDA_OK(cudaFree(device_x));
CUDA_OK(cudaFree(device_w));
CUDA_OK(cudaFree(device_dx));
CUDA_OK(cudaFree(device_dw));
CUDA_OK(cudaFree(device_target));
CUDA_OK(cudaFree(device_scalars));
}
} // namespace
int main(int argc, char** argv) {
if (argc == 6) {
int rank = std::atoi(argv[1]);
int ranks = std::atoi(argv[2]);
int device = std::atoi(argv[3]);
CUDA_OK(cudaSetDevice(device));
CUDA_OK(cudaFree(nullptr));
nvshmemx_uniqueid_t id = NVSHMEMX_UNIQUEID_INITIALIZER;
if (rank == 0 && nvshmemx_get_uniqueid(&id) != 0) return 1;
exchange_unique_id(
rank, ranks, argv[4], std::atoi(argv[5]), &id);
nvshmemx_init_attr_t attributes = NVSHMEMX_INIT_ATTR_INITIALIZER;
nvshmemx_set_attr_uniqueid_args(
rank, ranks, &id, &attributes);
if (nvshmemx_init_attr(
NVSHMEMX_INIT_WITH_UNIQUEID, &attributes) != 0) {
return 1;
}
} else {
nvshmem_init();
}
g_pe = nvshmem_my_pe();
g_n_pes = nvshmem_n_pes();
CUDA_OK(cudaSetDevice(nvshmem_team_my_pe(NVSHMEMX_TEAM_NODE)));
cudaDeviceProp properties = {};
CUDA_OK(cudaGetDeviceProperties(
&properties, nvshmem_team_my_pe(NVSHMEMX_TEAM_NODE)));
if (properties.major != 9) {
if (g_pe == 0) {
std::printf("skipping: requires Hopper (sm90), found sm_%d%d\n",
properties.major, properties.minor);
}
nvshmem_finalize();
return 0;
}
if (g_pe == 0) {
std::printf("tensor-parallel backward over %d PEs\n", g_n_pes);
}
check_chunk_ownership();
// Capacities and multicast mappings are immutable once allocated, so
// configure for the largest shape and the widest knobs used below.
// Collective.
fslce::configure_backward_tp_symmetric(
kMaxTokens, kMaxHidden, kMaxLocalVocab,
kMaxTilesPerReduce, kLegacyMaxCommChannels,
static_cast<std::int64_t>(NVSHMEM_TEAM_WORLD));
if (g_pe == 0) {
std::printf(
"symmetric staging %.2f MiB, device scratch %.2f MiB, "
"resident_ctas=%d nvls_team_size=%d\n",
fslce::backward_tp_pool_symmetric_bytes(
kMaxTokens, kMaxHidden,
kMaxTilesPerReduce, kLegacyMaxCommChannels) /
(1024.0 * 1024.0),
fslce::backward_tp_pool_device_bytes(kMaxLocalVocab) /
(1024.0 * 1024.0),
fslce::backward_dx_resident_cta_capacity(),
fslce::backward_dx_team_size());
}
// One N256 hidden tile: no grouping, exercises the simplest ring path.
const CaseShape small{256, 256, 512, 1.0f, 101};
// Three N256 hidden tiles with a ragged last one (648 = 2 * 256 + 136), so
// TilesPerReduce 2 produces a {2, 1} tail split and 4 a {3} tail.
const CaseShape ragged{200, 648, 333, 0.8f, 103};
// Two waves: exercises the dW TMA reduce-add path and a ring that wraps.
const CaseShape two_waves{1280, 512, 512, 1.0f, 105};
// Five K64 vocabulary tiles: H2048 must fall back to split-K1 rather than
// dropping the final odd tile. Non-unit temperature checks the chain rule.
const CaseShape odd_k_tiles{128, 2048, 320, 0.8f, 106};
// 136 dX groups with TPR=1 on a 132-SM H100: launches the full resident
// grid and makes four CTAs wrap to a second CTA-owned group.
const CaseShape full_grid{1, 4352, 64, 1.0f, 107};
// 136 two-tile chunks: the same full-grid wrap, now proving adjacent
// grouped messages alternate between the CTA's two comm warps.
const CaseShape full_grid_grouped{1, 8448, 64, 1.0f, 109};
try {
run_case<false>("dX/dW aligned, no entropy", small, 2, 2);
run_case<true>("dX/dW aligned, entropy", small, 2, 2);
run_case<true>("dX/dW ragged M/H/V, TilesPerReduce=1", ragged, 1, 2);
run_case<true>("dX/dW ragged M/H/V, TilesPerReduce=2", ragged, 2, 2);
run_case<true>("dX/dW ragged M/H/V, TilesPerReduce=4", ragged, 4, 2);
run_case<true>("dX/dW ragged, legacy channel arg=1", ragged, 2, 1);
run_case<true>("dX/dW ragged, legacy channel arg=4", ragged, 2, 4);
run_case<false>("dX/dW two waves, TilesPerReduce=2", two_waves, 2, 4);
run_case<true>(
"dX/dW H2048 odd K tiles and temperature",
odd_k_tiles,
1,
2);
run_case<false>("dX/dW full resident CTA grid", full_grid, 1, 2);
run_case<false>(
"dX/dW full grid, alternating two-tile chunks",
full_grid_grouped, 2, 2);
} catch (const std::exception& error) {
std::printf("[pe %d] exception: %s\n", g_pe, error.what());
fslce::reset_fslce_tp_configuration();
liger::global_buffer_pool().clear();
nvshmem_finalize();
return 1;
}
// Make every rank's verdict visible before PE 0 prints the summary.
// Pooled, not raw nvshmem_malloc: this module never runs its own symmetric
// allocation lifecycle.
auto& pool = liger::global_buffer_pool();
int* local_slot = static_cast<int*>(
pool.get_symmetric("fslce_bwd_selftest_verdict_local", sizeof(int)));
int* total_slot = static_cast<int*>(
pool.get_symmetric("fslce_bwd_selftest_verdict_total", sizeof(int)));
CUDA_OK(cudaMemcpy(
local_slot, &g_failures, sizeof(int), cudaMemcpyHostToDevice));
nvshmem_int_sum_reduce(NVSHMEM_TEAM_WORLD, total_slot, local_slot, 1);
int host_total = 0;
CUDA_OK(cudaMemcpy(
&host_total, total_slot, sizeof(int), cudaMemcpyDeviceToHost));
if (g_pe == 0) {
std::printf("%s: %d comparison failures across %d PEs\n",
host_total == 0 ? "OK" : "FAILED", host_total, g_n_pes);
}
// Release the pooled allocations while NVSHMEM is still up: the pool is a
// function-local static whose destructor would otherwise run after
// nvshmem_finalize().
fslce::reset_fslce_tp_configuration();
liger::global_buffer_pool().clear();
nvshmem_finalize();
return host_total == 0 ? 0 : 1;
}
#endif // LIGER_FSLCE_BACKWARD_SM90_SELFTEST
@@ -0,0 +1,62 @@
#pragma once
#include <cuda_runtime.h>
#include <cstdint>
namespace liger {
namespace fused_scaled_linear_cross_entropy {
inline constexpr int kWarpSize = 32;
inline constexpr int kNumThreads = 384;
inline constexpr int kNumWarps = kNumThreads / kWarpSize;
enum class BackwardWarpRole : std::uint8_t {
kProducer,
kDxCommunication,
kReserved,
kConsumer,
kInactive,
};
__host__ __device__ constexpr BackwardWarpRole backward_warp_role(int warp_id) {
return warp_id == 0
? BackwardWarpRole::kProducer
: (warp_id >= 1 && warp_id <= 2
? BackwardWarpRole::kDxCommunication
: (warp_id == 3
? BackwardWarpRole::kReserved
: (warp_id >= 4 && warp_id < kNumWarps
? BackwardWarpRole::kConsumer
: BackwardWarpRole::kInactive)));
}
struct DxTileGroup {
int m_tile;
int first_n_tile;
int num_tiles;
int sequence;
};
__host__ __device__ constexpr int ceil_div(int value, int divisor) {
return (value + divisor - 1) / divisor;
}
template <typename Config>
__host__ __device__ constexpr int dx_groups_per_m_tile(int num_n_tiles) {
return ceil_div(num_n_tiles, Config::kCoalesceTiles);
}
template <typename Config>
__host__ __device__ constexpr DxTileGroup make_dx_tile_group(
int m_tile, int group_in_m, int num_n_tiles, int sequence) {
int first_n_tile = group_in_m * Config::kCoalesceTiles;
int remaining = num_n_tiles - first_n_tile;
int num_tiles = remaining < Config::kCoalesceTiles
? remaining
: Config::kCoalesceTiles;
return {m_tile, first_n_tile, num_tiles > 0 ? num_tiles : 0, sequence};
}
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,240 @@
#pragma once
// ═══════════════════════════════════════════════════════════════════════════
// dX tensor-parallel reduction contract — CTA-owned staging, tile grouping
// and one-team NVLS synchronization.
//
// The fused SM90 backward kernel computes a *partial* dX on every rank: the
// vocabulary is sharded, so `dX_local = dZ_local @ W_local` is one term of a
// SUM over the tensor-parallel team. Instead of materialising the whole
// partial dX and running one big collective afterwards, each completed
// M128xN256 FP32 accumulator tile is written straight into a compact symmetric
// FP32 staging ring owned by that same CTA. Its own warps 1..2 reduce and
// scatter the group while the WGMMA consumers keep working on later groups.
//
// ── Pipeline ──────────────────────────────────────────────────────────────
// consumer epilogue FP32 accumulator -> generic coalesced global stores
// into partial[slot], CTA named-barrier release,
// then one CTA-scope release-store of a unique epoch
// owning comm warp local_chunk_index % 2 selects warp 1 or 2;
// it acquire-loads that epoch, publishes a block-prefixed
// system-scope NVLS ready epoch, performs the two-shot
// multicast SUM, then publishes a completion epoch
// owning comm warp vector-load the complete FP32 result,
// convert once to X.dtype (BF16 in this specialization),
// scatter into row-major dX,
// CTA-local consumed mbarrier arrival
//
// ── Grouping ──────────────────────────────────────────────────────────────
// `TilesPerReduce` contiguous N tiles of one M tile form one reduction
// message. The last group of an M tile is a ragged tail of
// `num_n_tiles % TilesPerReduce` tiles; its message is exactly that many tiles
// long, which is identical on every PE because every PE sees the same shapes.
//
// ── CTA ownership and independent NVLS regions ────────────────────────────
// Wave-local group `unit` belongs to CTA `unit % gridDim.x`; warp 0 and warps
// 4..11 produce only that CTA's groups, and warps 1..2 consume exactly the
// same sequence. Ring slot `j % NumStages` is therefore private to one CTA,
// and ready/reuse handoff is entirely CTA-local.
//
// Every ring slot has disjoint symmetric data and signal prefixes. All CTAs
// share one tensor-parallel team's immutable NVLS multicast mapping; no
// collective counter or team scratch is used inside the fused kernel.
//
// Warp 3 is reserved and idle on every path.
//
// This header stays NVSHMEM-free and CUTLASS-free so torch-free consumers can
// include the umbrella header.
// ═══════════════════════════════════════════════════════════════════════════
#include <cuda_runtime.h>
#include <cstddef>
#include <cstdint>
#include "config.cuh"
namespace liger {
namespace fused_scaled_linear_cross_entropy {
// Hopper has at most 132 resident SMs/CTAs for this 1-CTA/SM kernel.
inline constexpr int kMaxDxResidentCtas = 132;
inline constexpr int kDxCommWarpsPerChannel = 2;
inline constexpr int kDxSyncPhases = 2;
inline constexpr int kDxReadyPhase = 0;
inline constexpr int kDxCompletePhase = 1;
// Ring depth. Compile time: the kernel template, symmetric capacity and
// CTA-local mbarrier arrays all have to agree, so there is exactly one place
// to change it. 2, 3 and 4 are supported and exercised; 4 is the default
// because the comm warps run one pipeline step behind the collective, so a
// producer can run ahead by kDxRingStages - 1 groups.
inline constexpr int kDxRingStages = 4;
static_assert(kDxRingStages >= 2,
"the dX staging ring needs at least a double buffer");
// Compile-time shape of the staging ring.
//
// NumStages ring depth per CTA; >= 2 or the producer of a group
// would wait on the very slot it is about to fill.
// TilesPerReduce contiguous N tiles coalesced into one reduction message.
template <
typename GemmConfig,
int NumStages = 4,
int TilesPerReduce = 2,
int Compute = GemmConfig::kCompute>
struct DxCommConfig {
static_assert(Compute == 90, "DxCommConfig requires Compute=90");
static_assert(NumStages >= 2,
"the dX staging ring needs at least a double buffer; with one stage "
"the producer of group j would wait on group j itself");
static_assert(TilesPerReduce >= 1,
"TilesPerReduce must be positive");
static constexpr int kCompute = Compute;
static constexpr int kNumStages = NumStages;
static constexpr int kTilesPerReduce = TilesPerReduce;
// config.cuh's make_dx_tile_group()/dx_groups_per_m_tile() spell this
// knob kCoalesceTiles; keep the alias so both names name one value.
static constexpr int kCoalesceTiles = TilesPerReduce;
static constexpr int kTileM = GemmConfig::kTileM;
static constexpr int kTileN = GemmConfig::kDxTileN;
static constexpr int kTileElements = kTileM * kTileN;
static constexpr int kGroupElements = kTileElements * kTilesPerReduce;
static constexpr int kProducerWarp = 0;
static constexpr int kFirstCommWarp = 1;
static constexpr int kNumCommWarps = kDxCommWarpsPerChannel;
static constexpr int kLastCommWarp = kFirstCommWarp + kNumCommWarps - 1;
static constexpr int kReservedWarp = 3;
static_assert(kLastCommWarp + 1 == kReservedWarp,
"warps 1..2 cooperatively communicate and warp 3 stays reserved");
static_assert(backward_warp_role(kProducerWarp) ==
BackwardWarpRole::kProducer);
static_assert(backward_warp_role(kFirstCommWarp) ==
BackwardWarpRole::kDxCommunication);
static_assert(backward_warp_role(kLastCommWarp) ==
BackwardWarpRole::kDxCommunication);
static_assert(backward_warp_role(kReservedWarp) ==
BackwardWarpRole::kReserved);
};
// FSLCE-specific symmetric ring storage. Backend mappings are supplied through
// the minimal views in liger_cute/detail/tp_reduce.cuh.
//
// partial / reduced are symmetric (NVSHMEM) and laid out identically on every
// PE as
// [max_resident_ctas][2 comm warps][kNumStages][kTilesPerReduce][kTileM][kTileN],
// FP32 and
// fully packed, so one group is one contiguous message. FP32 is intentional:
// TP=1 and TP>1 use the same reduction path and accumulation semantics, and
// the final row-major grad_input store is the only conversion to X.dtype
// (BF16 in the SM90 specialization).
//
template <typename Element>
struct DxReduceWorkspace {
Element* partial; // symmetric, SUM source
Element* reduced; // symmetric, SUM destination
std::uint64_t* sync; // local symmetric signal replicas
const std::uint64_t* launch_epoch; // device-resident graph-safe generation
};
#if defined(__CUDACC__)
template <typename Element>
__device__ __forceinline__ std::uint64_t dx_epoch_base(
const DxReduceWorkspace<Element>& workspace) {
return *workspace.launch_epoch;
}
#endif
template <typename Config>
__host__ __device__ inline std::size_t dx_slot_index(
int cta, int comm_warp, int stage) {
return (static_cast<std::size_t>(cta) * kDxCommWarpsPerChannel +
comm_warp) *
Config::kNumStages +
stage;
}
// Element offset of a slot's first tile inside the packed staging arena. The
// compile-time communication config is the single source of truth for the
// staging stride used by all producers and consumers.
template <typename Config>
__host__ __device__ inline std::size_t dx_slot_offset(
int cta, int comm_warp, int stage) {
std::size_t slot = dx_slot_index<Config>(cta, comm_warp, stage);
return slot * static_cast<std::size_t>(Config::kTilesPerReduce) *
static_cast<std::size_t>(Config::kTileElements);
}
template <typename Config>
__host__ __device__ inline std::size_t dx_sync_offset(
int cta,
int comm_warp,
int stage,
int phase,
int team_size) {
std::size_t slot = dx_slot_index<Config>(cta, comm_warp, stage);
return (slot * kDxSyncPhases + static_cast<std::size_t>(phase)) *
static_cast<std::size_t>(team_size);
}
// ───────────────────────────────────────────────────────────────────────────
// Deterministic group schedule
// ───────────────────────────────────────────────────────────────────────────
// Index in one CTA's deterministic group sequence -> ring coordinates.
// Both communication warps cooperatively reduce disjoint halves of the same
// slot, so the payload always lives in data slot zero.
struct DxCtaGroupSlot {
int comm_warp;
int index_in_warp;
int stage;
int pass;
};
template <typename Config>
__host__ __device__ inline DxCtaGroupSlot dx_cta_group_slot(
int index_in_cta) {
DxCtaGroupSlot slot;
slot.comm_warp = 0;
slot.index_in_warp = index_in_cta;
slot.stage = slot.index_in_warp % Config::kNumStages;
slot.pass = slot.index_in_warp / Config::kNumStages;
return slot;
}
// Wave-local unit index -> the (m_tile, first_n_tile, num_tiles) it covers.
// `unit` counts M-fastest-last: unit = m_tile * groups_per_m + group_in_m.
template <typename Config>
__host__ __device__ inline DxTileGroup dx_unit_to_group(
int unit, int num_n_tiles, int group_id) {
int groups_per_m = dx_groups_per_m_tile<Config>(num_n_tiles);
int m_tile = unit / groups_per_m;
int group_in_m = unit - m_tile * groups_per_m;
return make_dx_tile_group<Config>(
m_tile, group_in_m, num_n_tiles, group_id);
}
__host__ __device__ inline int dx_groups_for_cta_per_wave(
int groups_per_wave, int num_ctas, int cta) {
return groups_per_wave > cta
? (groups_per_wave - cta + num_ctas - 1) / num_ctas
: 0;
}
// Global group -> index in its owning CTA's sequence across all waves.
__host__ __device__ inline int dx_cta_group_index(
int group_id, int groups_per_wave, int num_ctas) {
int wave = group_id / groups_per_wave;
int unit = group_id - wave * groups_per_wave;
int cta = unit % num_ctas;
int index_in_wave = unit / num_ctas;
return wave * dx_groups_for_cta_per_wave(
groups_per_wave, num_ctas, cta) + index_in_wave;
}
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,794 @@
#pragma once
// ═══════════════════════════════════════════════════════════════════════════
// SM90 fused scaled linear cross entropy — executable forward mainloop.
//
// Structural port of _fused_scaled_cross_entropy_forward_fragment_sm90.py.
// The CuTe/CUTLASS mechanics (TiledMMA construction, PipelineTmaAsync driving,
// TMA partitioning, warpgroup fences) follow the repository's SM90 idioms in
// csrc/core/src/moe, but the loop nest, work assignment, staging order,
// masking and online-softmax sequencing are a literal transcription of the
// reference kernel — not a generic CUTLASS GEMM.
//
// CTA layout (384 threads = 12 warps, cluster = (2, 1, 1)):
// warp 0 : TMA producer, 24 registers. Loads the CTA's own M128xK64 X
// tile and multicasts two N160xK64 W panels to the cluster
// peer through one 3-stage mbarrier pipeline.
// warps 1..3 : idle in forward (they carry the dX comms in backward), but
// they still take PRODUCER_REGISTERS so the warp group's
// setmaxnreg is uniform.
// warps 4..11 : two WGMMA consumer warp groups, 240 registers. Each owns 64
// token rows and two FP32 N160 accumulators.
//
// Mainloop: the consumer read state and the release state are deliberately
// separate. Stage zero is issued without a release; every later stage is
// committed, `warpgroup_wait<1>()` retires the previous WGMMA group, and only
// then is that previous AB stage returned to the producer. The tail uses
// `warpgroup_wait<0>()` before the final release. This one-stage lag keeps TMA
// from overwriting operands still feeding a live WGMMA group.
//
// Epilogue: each N160 accumulator is staged as two N80 FP16 SMEM slices, so a
// logical N320 tile produces four N80 online-softmax folds. Both padded
// staging buffers are fully consumed before either is overwritten. Two threads
// cooperate per token row (one N40 chunk each) and merge through SMEM once the
// CTA's vocabulary split has been folded.
// ═══════════════════════════════════════════════════════════════════════════
// gemm_sm90.cuh carries the reusable SM90 producer/consumer mechanics and
// enables CuTe's extended MMA shapes (where the N160 WGMMA atom lives), so it
// must be the first CUTLASS/CuTe include of any TU using this header.
#include "gemm_sm90.cuh"
#include "forward_gemm_sm90.cuh"
#include "online_softmax.cuh"
#include <cstdint>
namespace liger {
namespace fused_scaled_linear_cross_entropy {
using namespace cute;
// ───────────────────────────────────────────────────────────────────────────
// Traits
// ───────────────────────────────────────────────────────────────────────────
template <int Compute = 90>
struct ForwardGemmTraitsSm90 {
static_assert(Compute == 90,
"SM90 fused scaled linear cross entropy requires Compute=90");
using Config = ForwardGemmConfigSm90<Compute>;
static constexpr int kCompute = Compute;
using Element = cutlass::bfloat16_t;
using ElementAccum = float;
// The N80 epilogue staging slices are FP16, mirroring the reference's
// two padded Float16 SMEM buffers.
using ElementLogit = cutlass::half_t;
static constexpr int kTileM = Config::kTileM;
static constexpr int kTileK = Config::kTileK;
static constexpr int kAccumulatorTileN = Config::kAccumulatorTileN;
static constexpr int kLogicalTileN = Config::kLogicalTileN;
static constexpr int kEpilogueSliceN = Config::kEpilogueSliceN;
static constexpr int kSoftmaxChunkN = Config::kSoftmaxChunkN;
static constexpr int kSegmentN = Config::kSegmentN;
static constexpr int kChunksPerSegment = Config::kChunksPerSegment;
static constexpr int kStages = Config::kMainloopStages;
static constexpr int kClusterM = Config::kClusterM;
static constexpr int kNumAccumulators = Config::kNumAccumulators;
static constexpr int kNumThreads = Config::kNumThreads;
static constexpr int kWarpSize = fused_scaled_linear_cross_entropy::kWarpSize;
static constexpr int kWarpGroupSize = Config::kWarpGroupSize;
static constexpr int kNumMmaWarpGroups = Config::kNumMmaWarpGroups;
static constexpr int kConsumerThreads = Config::kConsumerThreads;
static constexpr int kLogitBuffers = Config::kLogitBuffers;
static constexpr int kLogitStride = Config::kLogitStride;
static constexpr int kRowsPerWarpGroup = Config::kRowsPerWarpGroup;
using ClusterShape = Shape<Int<kClusterM>, _1, _1>;
// hopper_helpers.make_trivial_tiled_mma(BF16, BF16, K, K, F32, (2,1,1),
// (64, ACCUMULATOR_N)): the M64 WGMMA atom, two warp groups split over M.
using TiledMmaTraits = sm90::TiledMmaMSplitSm90<
Element,
Element,
ElementAccum,
kAccumulatorTileN,
kNumMmaWarpGroups,
GMMA::Major::K,
GMMA::Major::K,
Compute>;
using TiledMma = typename TiledMmaTraits::Type;
// Pin the atom: the reference's N160 fragment must not silently decay to a
// stack of narrower WGMMA issues.
static_assert(
cute::is_same_v<
typename TiledMmaTraits::Atom,
SM90_64x160x16_F32BF16BF16_SS<GMMA::Major::K, GMMA::Major::K>>,
"the forward accumulator must map onto the single N160 WGMMA atom");
static_assert(TiledMmaTraits::kTileM == kTileM);
static_assert(TiledMmaTraits::kTileN == kAccumulatorTileN);
// make_smem_layout_a/b for K-major BF16 with a K64 tile => 128 B rows,
// hence the SW128 swizzle atom.
using SmemTraitsX =
sm90::SmemLayoutKMajorSm90<Element, kTileM, kTileK, kStages, Compute>;
using SmemTraitsW = sm90::SmemLayoutKMajorSm90<
Element, kAccumulatorTileN, kTileK, kStages, Compute>;
using SmemLayoutX1 = typename SmemTraitsX::Single;
using SmemLayoutW1 = typename SmemTraitsW::Single;
using SmemLayoutX = typename SmemTraitsX::Staged;
using SmemLayoutW = typename SmemTraitsW::Staged;
using PipelineTraits = sm90::MainloopPipelineSm90<kStages, Compute>;
using MainloopPipeline = typename PipelineTraits::Type;
using PipelineState = typename PipelineTraits::State;
static constexpr int kTmaTransBytesX = SmemTraitsX::kStageBytes;
static constexpr int kTmaTransBytesW = SmemTraitsW::kStageBytes;
// tx_count = x_stage_bytes + NUM_ACCUMULATORS * w_stage_bytes: X and both
// N160 panels share one mbarrier per stage.
static constexpr int kTmaTransBytes =
kTmaTransBytesX + kNumAccumulators * kTmaTransBytesW;
};
// ───────────────────────────────────────────────────────────────────────────
// Shared memory — same member order as the reference SharedStorage struct.
// ───────────────────────────────────────────────────────────────────────────
template <int Compute, bool ReturnEntropy>
struct ForwardGemmSmemSm90 {
using Traits = ForwardGemmTraitsSm90<Compute>;
using Element = typename Traits::Element;
using ElementLogit = typename Traits::ElementLogit;
static constexpr int kSmemX = cosize_v<typename Traits::SmemLayoutX>;
static constexpr int kSmemW = cosize_v<typename Traits::SmemLayoutW>;
static constexpr int kLogitElements =
Traits::kLogitBuffers * Traits::kTileM * Traits::kLogitStride;
static constexpr int kWeightedElements =
ReturnEntropy ? Traits::kTileM : 1;
alignas(16) typename Traits::MainloopPipeline::SharedStorage pipeline;
alignas(1024) Element smem_x[kSmemX];
alignas(1024) Element smem_w0[kSmemW];
alignas(1024) Element smem_w1[kSmemW];
alignas(1024) ElementLogit smem_logits[kLogitElements];
// Cross-segment exchange: segment 1 publishes, segment 0 merges.
alignas(16) float segment_max[Traits::kTileM];
alignas(16) float segment_sum[Traits::kTileM];
alignas(16) float segment_target[Traits::kTileM];
alignas(16) float segment_weighted[kWeightedElements];
CUTE_DEVICE Element* x_data() { return &smem_x[0]; }
CUTE_DEVICE Element* w0_data() { return &smem_w0[0]; }
CUTE_DEVICE Element* w1_data() { return &smem_w1[0]; }
// s_logits[buffer, row, :] over the (LOGIT_BUFFERS, TILE_M, LOGIT_STRIDE)
// padded layout.
CUTE_DEVICE ElementLogit* logit_row(int buffer, int row) {
return &smem_logits[
(buffer * Traits::kTileM + row) * Traits::kLogitStride];
}
};
// ───────────────────────────────────────────────────────────────────────────
// EPILOGUE_BARRIER_ID named barrier — the two consumer warp groups only.
// ───────────────────────────────────────────────────────────────────────────
template <int Compute = 90>
CUTE_DEVICE void forward_epilogue_barrier_sm90() {
using Config = ForwardGemmConfigSm90<Compute>;
sm90::named_barrier_sync_sm90<
Config::kEpilogueBarrierId, Config::kConsumerThreads, Compute>();
}
// ───────────────────────────────────────────────────────────────────────────
// Work assignment — port of the kernel's cluster_work decomposition.
// ───────────────────────────────────────────────────────────────────────────
struct ForwardGemmWorkSm90 {
int m_pair;
int split_id;
int split_count;
int raw_pid_m;
int pid_m;
int output_work;
bool store_enabled;
};
template <int Compute = 90>
CUTE_DEVICE ForwardGemmWorkSm90 forward_gemm_assign_work_sm90(
const ForwardGemmSplitSm90<Compute>& split,
int cluster_work,
int cluster_rank) {
using Config = ForwardGemmConfigSm90<Compute>;
int m_pair;
int split_id;
int split_count;
if (split.extra_m_pairs == 0) {
m_pair = cluster_work / split.base_split_n;
split_id = cluster_work % split.base_split_n;
split_count = split.base_split_n;
} else {
// The first extra_m_pairs M pairs carry one extra vocabulary split.
int extra_work = split.extra_m_pairs * split.split_n;
if (cluster_work < extra_work) {
m_pair = cluster_work / split.split_n;
split_id = cluster_work % split.split_n;
split_count = split.split_n;
} else {
int normal_work = cluster_work - extra_work;
m_pair = split.extra_m_pairs + normal_work / split.base_split_n;
split_id = normal_work % split.base_split_n;
split_count = split.base_split_n;
}
}
ForwardGemmWorkSm90 work;
work.m_pair = m_pair;
work.split_id = split_id;
work.split_count = split_count;
work.raw_pid_m = m_pair * Config::kClusterM + cluster_rank;
work.store_enabled = work.raw_pid_m < split.num_m_tiles;
// The tail CTA of an odd M-tile count still feeds the cluster pipeline;
// it re-reads the last valid M tile and drops its stores.
work.pid_m = work.store_enabled
? work.raw_pid_m
: (split.num_m_tiles - 1);
work.output_work = work.raw_pid_m * split.split_n + split_id;
return work;
}
// ───────────────────────────────────────────────────────────────────────────
// Producer — warp 0. X is CTA-private, both W panels are cluster-multicast.
// ───────────────────────────────────────────────────────────────────────────
template <int Compute = 90>
struct ForwardGemmProducerSm90 {
using Traits = ForwardGemmTraitsSm90<Compute>;
static constexpr int kCompute = Compute;
static_assert(Compute == 90,
"SM90 fused scaled linear cross entropy requires Compute=90");
template <bool ReturnEntropy, class Pipeline, class TmaLoadX, class TmaLoadW>
CUTE_DEVICE static void run(
Pipeline& pipe,
typename Traits::PipelineState& state,
ForwardGemmSmemSm90<Compute, ReturnEntropy>& smem,
TmaLoadX const& tma_load_x,
TmaLoadW const& tma_load_w,
ForwardGemmParamsSm90<Compute> const& params,
ForwardGemmWorkSm90 const& work,
int num_split_tiles,
int num_k_tiles) {
// prefetch_descriptor(tma_x) / prefetch_descriptor(tma_w): warm the
// descriptor cache from the whole producer warp before gating.
cute::prefetch_tma_descriptor(tma_load_x.get_tma_descriptor());
cute::prefetch_tma_descriptor(tma_load_w.get_tma_descriptor());
if (cute::elect_one_sync() == 0) return;
auto sX = make_tensor(
make_smem_ptr(smem.x_data()), typename Traits::SmemLayoutX{});
auto sW0 = make_tensor(
make_smem_ptr(smem.w0_data()), typename Traits::SmemLayoutW{});
auto sW1 = make_tensor(
make_smem_ptr(smem.w1_data()), typename Traits::SmemLayoutW{});
// int64 extents: tokens * hidden and local_vocab * hidden both exceed
// INT_MAX at production shapes and would wrap CuTe's layout algebra.
auto mX = tma_load_x.get_tma_tensor(make_shape(
static_cast<int64_t>(params.tokens),
static_cast<int64_t>(params.hidden)));
auto mW = tma_load_w.get_tma_tensor(make_shape(
static_cast<int64_t>(params.local_vocab),
static_cast<int64_t>(params.hidden)));
// X is not multicast; the W panels are multicast along the cluster's
// M mode, so this CTA issues its own slice of every W box.
uint32_t cluster_rank = cute::block_rank_in_cluster();
auto cta_tma_x = tma_load_x.get_slice(Int<0>{});
auto cta_tma_w = tma_load_w.get_slice(cluster_rank);
auto tXsX = cta_tma_x.partition_D(sX);
auto tW0sW0 = cta_tma_w.partition_D(sW0);
auto tW1sW1 = cta_tma_w.partition_D(sW1);
auto gX = local_tile(
mX,
make_tile(Int<Traits::kTileM>{}, Int<Traits::kTileK>{}),
make_coord(work.pid_m, _));
auto tXgX = cta_tma_x.partition_S(gX);
// make_layout_image_mask(cta_layout, cluster_coord, mode=0): the
// cluster is (ClusterM, 1, 1), so the W multicast covers every CTA.
uint16_t mcast_mask_w =
sm90::cluster_multicast_mask_m_sm90<
typename Traits::ClusterShape, Compute>();
for (int local_n = 0; local_n < num_split_tiles; ++local_n) {
int logical_n = work.split_id + local_n * work.split_count;
int fragment_base = logical_n * Traits::kNumAccumulators;
auto gW0 = local_tile(
mW,
make_tile(
Int<Traits::kAccumulatorTileN>{}, Int<Traits::kTileK>{}),
make_coord(fragment_base + 0, _));
auto gW1 = local_tile(
mW,
make_tile(
Int<Traits::kAccumulatorTileN>{}, Int<Traits::kTileK>{}),
make_coord(fragment_base + 1, _));
auto tW0gW0 = cta_tma_w.partition_S(gW0);
auto tW1gW1 = cta_tma_w.partition_S(gW1);
// One mbarrier per stage covers X and both N160 panels.
for (int k_tile = 0; k_tile < num_k_tiles; ++k_tile) {
pipe.producer_acquire(state);
auto* barrier = pipe.producer_get_barrier(state);
int stage = state.index();
copy(tma_load_x.with(*barrier, 0),
tXgX(_, _, _, k_tile), tXsX(_, _, _, stage));
copy(tma_load_w.with(*barrier, mcast_mask_w),
tW0gW0(_, _, _, k_tile), tW0sW0(_, _, _, stage));
copy(tma_load_w.with(*barrier, mcast_mask_w),
tW1gW1(_, _, _, k_tile), tW1sW1(_, _, _, stage));
++state;
}
}
// producer_tail: keeps this CTA from retiring while a cluster peer
// still holds operands multicast out of it.
pipe.producer_tail(state);
}
};
// ───────────────────────────────────────────────────────────────────────────
// Consumer — warps 4..11.
// ───────────────────────────────────────────────────────────────────────────
template <int Compute = 90>
struct ForwardGemmConsumerSm90 {
using Traits = ForwardGemmTraitsSm90<Compute>;
using Config = typename Traits::Config;
using Epilogue = ForwardGemmEpilogueSm90<Compute>;
static constexpr int kCompute = Compute;
static_assert(Compute == 90,
"SM90 fused scaled linear cross entropy requires Compute=90");
// _stage_slice: scatter one N80 slice of an FP32 accumulator into a padded
// FP16 staging buffer, then publish it to both consumer warp groups.
// coord_c is thr_mma.partition_C over the identity tensor, which reproduces
// the reference's explicit row_base / col_base fragment arithmetic.
template <bool ReturnEntropy, int SliceBase, class Accum, class CoordC>
CUTE_DEVICE static void stage_slice(
ForwardGemmSmemSm90<Compute, ReturnEntropy>& smem,
Accum const& accum,
CoordC const& coord_c,
int buffer) {
CUTE_UNROLL
for (int i = 0; i < size(accum); ++i) {
int row = get<0>(coord_c(i));
int col = get<1>(coord_c(i));
if (col >= SliceBase &&
col < SliceBase + Traits::kEpilogueSliceN) {
smem.logit_row(buffer, row)[col - SliceBase] =
static_cast<typename Traits::ElementLogit>(accum(i));
}
}
cutlass::arch::fence_view_async_shared();
asm volatile("fence.acq_rel.cta;" ::: "memory");
forward_epilogue_barrier_sm90<Compute>();
}
// _fold_slice: fold this thread's N40 segment of a staged N80 slice into
// the rolling statistics, then pick up the target logit if it lands here.
// `valid_cols = local_vocab - slice_vocab_base` may be negative, matching
// the reference; the chunk load keeps its mask-free fast path.
template <bool ReturnEntropy>
CUTE_DEVICE static void fold_slice(
ForwardGemmSmemSm90<Compute, ReturnEntropy>& smem,
OnlineSoftmaxState& softmax,
int buffer,
int row,
int segment,
int slice_vocab_base,
int valid_cols,
int target_col,
float inverse_temperature) {
constexpr int kChunkN = Traits::kSoftmaxChunkN;
constexpr int kSegmentN = Traits::kSegmentN;
int segment_base = segment * kSegmentN;
const typename Traits::ElementLogit* logits =
smem.logit_row(buffer, row);
CUTE_UNROLL
for (int ci = 0; ci < Traits::kChunksPerSegment; ++ci) {
int c0 = segment_base + ci * kChunkN;
float values[kChunkN];
float chunk_max = kForwardMaskLogit;
if (c0 + kChunkN <= valid_cols) {
CUTE_UNROLL
for (int j = 0; j < kChunkN; ++j) {
float value = static_cast<float>(logits[c0 + j]) *
inverse_temperature;
values[j] = value;
chunk_max = fmaxf(chunk_max, value);
}
} else {
CUTE_UNROLL
for (int j = 0; j < kChunkN; ++j) {
float value = static_cast<float>(logits[c0 + j]) *
inverse_temperature;
if (c0 + j >= valid_cols) value = kForwardMaskLogit;
values[j] = value;
chunk_max = fmaxf(chunk_max, value);
}
}
Epilogue::template fold_chunk<ReturnEntropy, kChunkN>(
softmax, values, chunk_max);
}
// The reference re-reads the target logit from SMEM rather than
// indexing the chunk registers, which would demote them to local
// memory. target_col is already a shard-local column index.
int target_begin = slice_vocab_base + segment_base;
int target_end = target_begin + kSegmentN;
if (target_col >= target_begin && target_col < target_end) {
softmax.target_logit +=
static_cast<float>(logits[target_col - slice_vocab_base]) *
inverse_temperature;
softmax.has_target = 1;
}
}
template <bool ReturnEntropy, class Pipeline>
CUTE_DEVICE static void run(
Pipeline& pipe,
typename Traits::PipelineState& state,
ForwardGemmSmemSm90<Compute, ReturnEntropy>& smem,
ForwardGemmParamsSm90<Compute> const& params,
ForwardGemmPartialsSm90<Compute> const& partials,
ForwardGemmWorkSm90 const& work,
int num_split_tiles,
int num_k_tiles) {
typename Traits::TiledMma tiled_mma;
int tid_in_mma =
static_cast<int>(threadIdx.x) - Traits::kWarpGroupSize; // 0..255
auto thr_mma = tiled_mma.get_slice(tid_in_mma);
auto sX = make_tensor(
make_smem_ptr(smem.x_data()), typename Traits::SmemLayoutX{});
auto sW0 = make_tensor(
make_smem_ptr(smem.w0_data()), typename Traits::SmemLayoutW{});
auto sW1 = make_tensor(
make_smem_ptr(smem.w1_data()), typename Traits::SmemLayoutW{});
auto tCsX = thr_mma.partition_A(sX);
auto tCsW0 = thr_mma.partition_B(sW0);
auto tCsW1 = thr_mma.partition_B(sW1);
using AccumShape =
Shape<Int<Traits::kTileM>, Int<Traits::kAccumulatorTileN>>;
auto accum0 = partition_fragment_C(tiled_mma, AccumShape{});
auto accum1 = partition_fragment_C(tiled_mma, AccumShape{});
OnlineSoftmaxState softmax;
softmax.max_value = kForwardNegInf;
auto state_release = state;
for (int local_n = 0; local_n < num_split_tiles; ++local_n) {
int logical_n = work.split_id + local_n * work.split_count;
clear(accum0);
clear(accum1);
// Two N160 fragments per K stage with the CuTe-DSL one-stage
// release lag: stage 0 is not released until stage 1 retires.
pipe.consumer_wait(state);
warpgroup_fence_operand(accum0);
warpgroup_fence_operand(accum1);
warpgroup_arrive();
gemm(tiled_mma, tCsX(_, _, _, state.index()),
tCsW0(_, _, _, state.index()), accum0);
gemm(tiled_mma, tCsX(_, _, _, state.index()),
tCsW1(_, _, _, state.index()), accum1);
warpgroup_commit_batch();
++state;
for (int k_tile = 1; k_tile < num_k_tiles; ++k_tile) {
pipe.consumer_wait(state);
warpgroup_fence_operand(accum0);
warpgroup_fence_operand(accum1);
warpgroup_arrive();
gemm(tiled_mma, tCsX(_, _, _, state.index()),
tCsW0(_, _, _, state.index()), accum0);
gemm(tiled_mma, tCsX(_, _, _, state.index()),
tCsW1(_, _, _, state.index()), accum1);
warpgroup_commit_batch();
warpgroup_wait<1>();
warpgroup_fence_operand(accum0);
warpgroup_fence_operand(accum1);
pipe.consumer_release(state_release);
++state_release;
++state;
}
warpgroup_wait<0>();
warpgroup_fence_operand(accum0);
warpgroup_fence_operand(accum1);
pipe.consumer_release(state_release);
++state_release;
auto coord_c =
thr_mma.partition_C(make_identity_tensor(AccumShape{}));
int tid_in_wg = tid_in_mma % Traits::kWarpGroupSize;
int mma_wg = tid_in_mma / Traits::kWarpGroupSize;
int epi_row = mma_wg * Traits::kRowsPerWarpGroup +
(tid_in_wg % Traits::kRowsPerWarpGroup);
int epi_segment = tid_in_wg / Traits::kRowsPerWarpGroup;
int global_row = work.raw_pid_m * Traits::kTileM + epi_row;
// Keep target addressing out of the live WGMMA region.
int target_col = -1;
if (work.store_enabled && global_row < params.tokens) {
std::int64_t target_id = params.target[global_row];
if (target_id != params.ignore_index) {
std::int64_t local = target_id - params.vocab_start;
if (local >= 0 &&
local < static_cast<std::int64_t>(params.local_vocab)) {
target_col = static_cast<int>(local);
}
}
}
constexpr int kSliceN = Traits::kEpilogueSliceN;
constexpr int kAccumN = Traits::kAccumulatorTileN;
int logical_base = logical_n * Traits::kLogicalTileN;
int local_vocab = params.local_vocab;
// Each N160 accumulator is folded as two N80 slices. Both buffers
// are fully consumed before either is overwritten.
stage_slice<ReturnEntropy, 0>(smem, accum0, coord_c, 0);
stage_slice<ReturnEntropy, kSliceN>(smem, accum0, coord_c, 1);
fold_slice<ReturnEntropy>(
smem, softmax, 0, epi_row, epi_segment,
logical_base,
local_vocab - logical_base,
target_col, params.inverse_temperature);
forward_epilogue_barrier_sm90<Compute>();
stage_slice<ReturnEntropy, 0>(smem, accum1, coord_c, 0);
fold_slice<ReturnEntropy>(
smem, softmax, 1, epi_row, epi_segment,
logical_base + kSliceN,
local_vocab - (logical_base + kSliceN),
target_col, params.inverse_temperature);
forward_epilogue_barrier_sm90<Compute>();
stage_slice<ReturnEntropy, kSliceN>(smem, accum1, coord_c, 1);
fold_slice<ReturnEntropy>(
smem, softmax, 0, epi_row, epi_segment,
logical_base + kAccumN,
local_vocab - (logical_base + kAccumN),
target_col, params.inverse_temperature);
forward_epilogue_barrier_sm90<Compute>();
fold_slice<ReturnEntropy>(
smem, softmax, 1, epi_row, epi_segment,
logical_base + kAccumN + kSliceN,
local_vocab - (logical_base + kAccumN + kSliceN),
target_col, params.inverse_temperature);
forward_epilogue_barrier_sm90<Compute>();
}
// Merge the two N40 segment halves of every token row and emit this
// CTA's split partials.
int tid_in_wg = tid_in_mma % Traits::kWarpGroupSize;
int mma_wg = tid_in_mma / Traits::kWarpGroupSize;
int epi_row = mma_wg * Traits::kRowsPerWarpGroup +
(tid_in_wg % Traits::kRowsPerWarpGroup);
int epi_segment = tid_in_wg / Traits::kRowsPerWarpGroup;
if (epi_segment == 1) {
smem.segment_max[epi_row] = softmax.max_value;
smem.segment_sum[epi_row] = softmax.exp_sum;
smem.segment_target[epi_row] = softmax.target_logit;
if constexpr (ReturnEntropy) {
smem.segment_weighted[epi_row] = softmax.exp_weighted_sum;
}
}
forward_epilogue_barrier_sm90<Compute>();
if (epi_segment == 0 && work.store_enabled) {
OnlineSoftmaxState other;
other.max_value = smem.segment_max[epi_row];
other.exp_sum = smem.segment_sum[epi_row];
other.target_logit = smem.segment_target[epi_row];
other.has_target = 0;
if constexpr (ReturnEntropy) {
other.exp_weighted_sum = smem.segment_weighted[epi_row];
}
OnlineSoftmaxState combined =
Epilogue::template combine_scaled<ReturnEntropy>(
softmax, other);
Epilogue::template store_partial<ReturnEntropy>(
combined,
partials,
work.output_work * Traits::kTileM + epi_row);
}
}
};
// ───────────────────────────────────────────────────────────────────────────
// Fragment kernel
// ───────────────────────────────────────────────────────────────────────────
template <bool ReturnEntropy, int Compute, class TmaLoadX, class TmaLoadW>
__global__ __launch_bounds__(ForwardGemmTraitsSm90<Compute>::kNumThreads, 1)
void forward_gemm_kernel_sm90(
__grid_constant__ const TmaLoadX tma_load_x,
__grid_constant__ const TmaLoadW tma_load_w,
__grid_constant__ const ForwardGemmParamsSm90<Compute> params,
__grid_constant__ const ForwardGemmPartialsSm90<Compute> partials,
__grid_constant__ const ForwardGemmSplitSm90<Compute> split) {
using Traits = ForwardGemmTraitsSm90<Compute>;
using Config = typename Traits::Config;
using Launch = ForwardGemmLaunchSm90<Compute>;
using Smem = ForwardGemmSmemSm90<Compute, ReturnEntropy>;
using PipelineTraits = typename Traits::PipelineTraits;
using ClusterShape = typename Traits::ClusterShape;
using Registers = sm90::WarpSpecializedRegistersSm90<
Config::kProducerRegisters, Config::kMmaRegisters, Compute>;
extern __shared__ char raw_smem[];
Smem& smem = *reinterpret_cast<Smem*>(raw_smem);
int warp_group = cutlass::canonical_warp_group_idx();
int warp = cutlass::canonical_warp_idx_sync();
// mbarrier init (stores are warp-0 only, the fence is everyone) followed by
// the cluster-wide handshake. No pipeline object exists yet, so nothing
// role-specific is live across this point.
PipelineTraits::init_barriers(
smem.pipeline,
Traits::kTmaTransBytes,
Traits::kConsumerThreads,
ClusterShape{});
sm90::cluster_pipeline_init_sm90<Compute>();
// Each role builds its own pipeline object, work assignment and tile
// counts inside a tight scope. The work/tile integers are a handful of
// scalar ops, so recomputing them per role is cheaper than keeping them
// live across the branch.
if (warp_group == 0) {
Registers::producer();
if (warp == 0) {
auto pipe = PipelineTraits::make_producer(
smem.pipeline,
Traits::kTmaTransBytes,
Traits::kConsumerThreads,
threadIdx.x == 0,
ClusterShape{});
typename Traits::PipelineState producer_state =
PipelineTraits::producer_start_state();
ForwardGemmWorkSm90 work = forward_gemm_assign_work_sm90<Compute>(
split,
static_cast<int>(blockIdx.z),
static_cast<int>(blockIdx.x));
int num_k_tiles = Launch::num_k_tiles(params.hidden);
int num_split_tiles = ceil_div(
split.num_logical_n_tiles - work.split_id, work.split_count);
ForwardGemmProducerSm90<Compute>::template run<ReturnEntropy>(
pipe,
producer_state,
smem,
tma_load_x,
tma_load_w,
params,
work,
num_split_tiles,
num_k_tiles);
}
} else {
Registers::consumer();
auto pipe = PipelineTraits::make_consumer(
smem.pipeline,
Traits::kTmaTransBytes,
Traits::kConsumerThreads,
ClusterShape{});
typename Traits::PipelineState consumer_state;
ForwardGemmWorkSm90 work = forward_gemm_assign_work_sm90<Compute>(
split,
static_cast<int>(blockIdx.z),
static_cast<int>(blockIdx.x));
int num_k_tiles = Launch::num_k_tiles(params.hidden);
int num_split_tiles = ceil_div(
split.num_logical_n_tiles - work.split_id, work.split_count);
ForwardGemmConsumerSm90<Compute>::template run<ReturnEntropy>(
pipe,
consumer_state,
smem,
params,
partials,
work,
num_split_tiles,
num_k_tiles);
}
sm90::cluster_exit_sm90<Compute>();
}
// ───────────────────────────────────────────────────────────────────────────
// Split reducer — the reference finalizer, stopped at *local* statistics so
// The host forward wrapper reduces across the tensor-parallel group.
// ───────────────────────────────────────────────────────────────────────────
template <bool ReturnEntropy, int Compute>
__global__ __launch_bounds__(ForwardGemmConfigSm90<Compute>::kTileM, 1)
void forward_split_reduce_kernel_sm90(
__grid_constant__ const ForwardGemmParamsSm90<Compute> params,
__grid_constant__ const ForwardGemmPartialsSm90<Compute> partials,
__grid_constant__ const ForwardGemmSplitSm90<Compute> split) {
using Config = ForwardGemmConfigSm90<Compute>;
using Epilogue = ForwardGemmEpilogueSm90<Compute>;
int tid = static_cast<int>(threadIdx.x);
int pid_m = static_cast<int>(blockIdx.x);
int row = pid_m * Config::kTileM + tid;
if (row >= params.tokens) return;
int split_count = split.split_count_for_pair(pid_m / Config::kClusterM);
float row_max = kForwardNegInf;
float row_target = 0.0f;
for (int split_id = 0; split_id < split_count; ++split_id) {
int index = (pid_m * split.split_n + split_id) * Config::kTileM + tid;
row_max = fmaxf(row_max, partials.partial_max[index]);
row_target += partials.partial_target[index];
}
float row_sum = 0.0f;
float row_weighted = 0.0f;
for (int split_id = 0; split_id < split_count; ++split_id) {
int index = (pid_m * split.split_n + split_id) * Config::kTileM + tid;
float split_scale = forward_exp2_sm90(
(partials.partial_max[index] - row_max) * kForwardLog2E);
row_sum += partials.partial_sum[index] * split_scale;
if constexpr (ReturnEntropy) {
row_weighted += partials.partial_weighted[index] * split_scale;
}
}
OnlineSoftmaxState state;
state.max_value = row_max;
state.exp_sum = row_sum;
state.target_logit = row_target;
if constexpr (ReturnEntropy) {
state.exp_weighted_sum = row_weighted;
}
// has_target is recomputed rather than carried: both segment threads see
// the same shard-local column, so it is a pure function of the target id.
std::int64_t target_id = params.target[row];
std::int64_t local = target_id - params.vocab_start;
state.has_target = (target_id != params.ignore_index && local >= 0 &&
local < static_cast<std::int64_t>(params.local_vocab)) ? 1 : 0;
Epilogue::template store_row<ReturnEntropy>(state, params.output, row);
}
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,523 @@
#pragma once
// ═══════════════════════════════════════════════════════════════════════════
// SM90 fused scaled linear cross entropy — forward GEMM contract.
//
// Structural port of
// src/liger_kernel/ops/cutedsl/ops/_fused_scaled_cross_entropy_forward_fragment_sm90.py
// (the CuTe-DSL "two-fragment" fragment kernel). Constant names below map 1:1
// onto the reference module constants:
//
// THREADS_PER_CTA 384 TILE_M 128 TILE_K 64 STAGES 3
// CLUSTER_M 2 ACCUMULATOR_N 160 NUM_ACCUMULATORS 2
// LOGICAL_N 320 EPILOGUE_SLICE_N 80 CHUNK_N 40
// LOGIT_BUFFERS 2 LOGIT_PAD 8 LOGIT_STRIDE 88
// EPILOGUE_BARRIER_ID 4 PRODUCER_REGISTERS 24 MMA_REGISTERS 240
//
// This header is deliberately CuTe-free: it carries the launch geometry, the
// vocabulary-split scheduler, the warp plan, the online-softmax epilogue
// contract and the host launcher declaration so torch-free consumers (and the
// existing C++ unit tests) can include the umbrella header without CUTLASS.
//
// The executable producer / consumer / mainloop is in
// forward_gemm_mainloop_sm90.cuh and is instantiated by
// fused_linear_scaled_cross_entropy_forward.cu.
//
// Tensor-parallel deltas from the single-GPU reference — everything else is a
// literal port:
// * W is the *local* vocabulary shard W_local[V_local, H]; local column c is
// global vocabulary index vocab_start + c.
// * `target` holds *global* int64 indices, mapped to the shard on the fly.
// * The reference's finalizer emits NLL/LSE/entropy directly. Here the split
// reducer stops at per-token *local* (max, sum, lse, target[, weighted])
// statistics; fused_linear_scaled_cross_entropy_forward corrects and reduces them
// across the NVSHMEM team.
// * The reference host-pads H to a multiple of TILE_K. TMA out-of-bounds
// zero fill covers the K tail here instead, so no padded copy is made.
// H only has to keep the BF16 row stride 16 B aligned (H % 8 == 0).
//
// The [M, V] logit matrix is never materialised in HBM.
// ═══════════════════════════════════════════════════════════════════════════
#include <cuda_runtime.h>
#include <cstddef>
#include <cstdint>
#include "config.cuh"
#include "online_softmax.cuh"
namespace liger {
namespace fused_scaled_linear_cross_entropy {
struct ForwardLocalStatsBuffers {
float* local_max;
float* local_sum;
float* local_target;
float* local_weighted_sum;
};
// _fused_scaled_cross_entropy_utils_sm90.py constants.
inline constexpr float kForwardLog2E = 1.4426950408889634f;
inline constexpr float kForwardLn2 = 0.6931471805599453f;
// Rolling-max seed. Strictly greater than kForwardMaskLogit so a fold made
// entirely of masked vocabulary columns leaves the running state untouched.
inline constexpr float kForwardNegInf = -1.0e38f;
inline constexpr float kForwardMaskLogit = -3.0e38f;
inline constexpr int kHopperMaxSmemBytes = 227 * 1024;
// exp2 with the reference's fastmath=True lowering (llvm.nvvm.ex2.approx.ftz).
__host__ __device__ inline float forward_exp2_sm90(float value) {
#if defined(__CUDA_ARCH__)
float result;
asm("ex2.approx.ftz.f32 %0, %1;" : "=f"(result) : "f"(value));
return result;
#else
return exp2f(value);
#endif
}
enum class ForwardWarpRole : std::uint8_t {
kProducer,
kIdle,
kConsumer,
kInactive,
};
// PRODUCER_WARP_GROUP = 0 owns warp 0 (TMA). Warps 1..3 sit in the producer
// warp group but are idle in forward — they carry the dX comms in backward.
// Warps 4..11 are the two WGMMA consumer warp groups.
__host__ __device__ constexpr ForwardWarpRole forward_warp_role(int warp_id) {
return warp_id == 0
? ForwardWarpRole::kProducer
: (warp_id >= 1 && warp_id <= 3
? ForwardWarpRole::kIdle
: (warp_id >= 4 && warp_id < kNumWarps
? ForwardWarpRole::kConsumer
: ForwardWarpRole::kInactive));
}
template <int Compute = 90>
struct ForwardGemmConfigSm90 {
static_assert(Compute == 90,
"SM90 fused scaled linear cross entropy requires Compute=90");
static constexpr int kCompute = Compute;
static constexpr int kTileM = 128;
static constexpr int kTileK = 64;
static constexpr int kLogicalTileN = 320;
static constexpr int kAccumulatorTileN = 160;
static constexpr int kEpilogueSliceN = 80;
static constexpr int kSoftmaxChunkN = 40;
static constexpr int kMainloopStages = 3;
static constexpr int kClusterM = 2;
static constexpr int kNumThreads =
fused_scaled_linear_cross_entropy::kNumThreads;
// Two N160 accumulators per consumer warp group form the logical N320.
static constexpr int kNumAccumulators = kLogicalTileN / kAccumulatorTileN;
static constexpr int kNumMmaWarpGroups = 2;
static constexpr int kWarpsPerWarpGroup = 4;
static constexpr int kWarpGroupSize = 128;
static constexpr int kConsumerThreads =
kNumMmaWarpGroups * kWarpGroupSize;
static constexpr int kMmaAtomTileM = kTileM / kNumMmaWarpGroups;
// Two ping-pong N80 staging buffers, padded to break SMEM bank conflicts.
static constexpr int kLogitBuffers = 2;
static constexpr int kLogitPad = 8;
static constexpr int kLogitStride = kEpilogueSliceN + kLogitPad;
// Two threads cooperate on each token row; each owns one N40 chunk of the
// N80 slice currently being folded.
static constexpr int kSegmentN = kEpilogueSliceN / 2;
static constexpr int kChunksPerSegment = kSegmentN / kSoftmaxChunkN;
static constexpr int kSegmentsPerRow = kEpilogueSliceN / kSegmentN;
static constexpr int kRowsPerWarpGroup = kTileM / kNumMmaWarpGroups;
static constexpr int kProducerRegisters = 24;
static constexpr int kMmaRegisters = 240;
static constexpr int kUsableRegisterBudget = 64512;
// Hardware named barrier shared by the two consumer warp groups.
static constexpr int kEpilogueBarrierId = 4;
// _resolve_split's ceiling on the vocabulary split (config.max_split_n).
static constexpr int kDefaultMaxSplitN = 9;
static_assert(kLogicalTileN == 2 * kAccumulatorTileN);
static_assert(kAccumulatorTileN == 2 * kEpilogueSliceN);
static_assert(kEpilogueSliceN == 2 * kSoftmaxChunkN);
static_assert(kSegmentN == kSoftmaxChunkN);
static_assert(kNumThreads == (kNumMmaWarpGroups + 1) * kWarpGroupSize);
static_assert(forward_warp_role(0) == ForwardWarpRole::kProducer);
static_assert(forward_warp_role(3) == ForwardWarpRole::kIdle);
static_assert(forward_warp_role(4) == ForwardWarpRole::kConsumer);
__host__ __device__ static constexpr int register_total() {
return kWarpGroupSize * kProducerRegisters +
kNumMmaWarpGroups * kWarpGroupSize * kMmaRegisters;
}
static_assert(register_total() <= kUsableRegisterBudget,
"SM90 forward register budget exceeds the usable per-CTA budget");
};
// ───────────────────────────────────────────────────────────────────────────
// Vocabulary-split scheduler — port of _resolve_split plus the kernel-side
// cluster_work decomposition.
// ───────────────────────────────────────────────────────────────────────────
// ScaledCEForwardFragmentConfig's tuning knobs. Zero means "auto".
template <int Compute = 90>
struct ForwardGemmTuningSm90 {
using Config = ForwardGemmConfigSm90<Compute>;
static constexpr int kCompute = Compute;
int split_n = 0;
int base_split_n = 0;
int extra_m_pairs = 0;
int target_cluster_pairs = 0;
int max_split_n = Config::kDefaultMaxSplitN;
};
template <int Compute = 90>
struct ForwardGemmSplitSm90 {
using Config = ForwardGemmConfigSm90<Compute>;
static constexpr int kCompute = Compute;
int split_n = 1;
int base_split_n = 1;
int extra_m_pairs = 0;
int num_m_tiles = 0;
int num_m_pairs = 0;
int num_logical_n_tiles = 0;
int num_cluster_pairs = 0;
// Number of vocabulary splits that actually cover `m_pair`. M pairs below
// extra_m_pairs get the extra split; the rest get base_split_n.
__host__ __device__ int split_count_for_pair(int m_pair) const {
if (extra_m_pairs == 0) return base_split_n;
return m_pair < extra_m_pairs ? split_n : base_split_n;
}
};
// FP32 (max, sum, target[, weighted]) split partials, laid out
// [num_m_tiles * split_n, TILE_M] exactly like the reference's
// partial_* tensors.
template <int Compute = 90>
struct ForwardGemmPartialsSm90 {
static constexpr int kCompute = Compute;
float* partial_max = nullptr;
float* partial_sum = nullptr;
float* partial_target = nullptr;
float* partial_weighted = nullptr;
};
// Problem + output description. The vocabulary shard is contiguous: local
// column c is global vocabulary index `vocab_start + c`, and targets arrive as
// *global* int64 indices.
template <int Compute = 90>
struct ForwardGemmParamsSm90 {
using Config = ForwardGemmConfigSm90<Compute>;
static constexpr int kCompute = Compute;
const void* x = nullptr; // BF16 [tokens, hidden], row major
const void* weight = nullptr; // BF16 [local_vocab, hidden], row major
const std::int64_t* target = nullptr; // int64 [tokens], global indices
ForwardLocalStatsBuffers output = {}; // FP32 per-token local statistics
// Split partials scratch; size it with
// ForwardGemmLaunchSm90<Compute>::workspace_bytes(...).
void* workspace = nullptr;
std::size_t workspace_bytes = 0;
int tokens = 0;
int hidden = 0;
int local_vocab = 0;
std::int64_t vocab_start = 0;
std::int64_t ignore_index = -100;
float inverse_temperature = 1.0f;
ForwardGemmTuningSm90<Compute> tuning = {};
};
// ───────────────────────────────────────────────────────────────────────────
// Online-softmax epilogue contract
// ───────────────────────────────────────────────────────────────────────────
template <int Compute = 90>
struct ForwardGemmEpilogueSm90 {
using Config = ForwardGemmConfigSm90<Compute>;
static constexpr int kCompute = Compute;
static_assert(Compute == 90,
"SM90 fused scaled linear cross entropy requires Compute=90");
// Scalar reference fold: one online-softmax update per logit. Kept as the
// readable contract; the device path uses the chunked form below.
template <bool ReturnEntropy, int FragmentSize>
__host__ __device__ static void fold_fragment(
OnlineSoftmaxState& state,
const float (&scaled_logits)[FragmentSize],
int fragment_vocab_start,
int local_vocab_size,
int local_target) {
#pragma unroll
for (int i = 0; i < FragmentSize; ++i) {
int local_vocab_index = fragment_vocab_start + i;
state.template fold<ReturnEntropy>(
scaled_logits[i],
local_vocab_index < local_vocab_size,
local_vocab_index == local_target);
}
}
// _fold_slice's rolling update, verbatim: one rescale of the running
// (sum[, weighted]) per N40 chunk. `values` is the chunk already masked
// and scaled by the caller and `chunk_max` its maximum, matching the
// reference's two-branch load.
template <bool ReturnEntropy, int ChunkN>
__host__ __device__ static void fold_chunk(
OnlineSoftmaxState& state,
const float (&values)[ChunkN],
float chunk_max) {
float next_max = fmaxf(state.max_value, chunk_max);
float chunk_sum = 0.0f;
if constexpr (ReturnEntropy) {
float chunk_weighted = 0.0f;
#pragma unroll
for (int i = 0; i < ChunkN; ++i) {
float weight =
forward_exp2_sm90((values[i] - next_max) * kForwardLog2E);
chunk_sum += weight;
chunk_weighted += weight * values[i];
}
float previous_scale = forward_exp2_sm90(
(state.max_value - next_max) * kForwardLog2E);
state.exp_sum = state.exp_sum * previous_scale + chunk_sum;
state.exp_weighted_sum =
state.exp_weighted_sum * previous_scale + chunk_weighted;
} else {
#pragma unroll
for (int i = 0; i < ChunkN; ++i) {
chunk_sum +=
forward_exp2_sm90((values[i] - next_max) * kForwardLog2E);
}
state.exp_sum = state.exp_sum * forward_exp2_sm90(
(state.max_value - next_max) * kForwardLog2E) + chunk_sum;
}
state.max_value = next_max;
}
// Branch-free merge of two rolling states, matching the reference's
// cross-segment combine and its split finalizer. Deliberately not
// merge_online_softmax(): that shortcuts on exp_sum == 0 and would put
// data-dependent branches in the epilogue.
template <bool ReturnEntropy>
__host__ __device__ static OnlineSoftmaxState combine_scaled(
const OnlineSoftmaxState& lhs, const OnlineSoftmaxState& rhs) {
OnlineSoftmaxState result;
result.max_value = fmaxf(lhs.max_value, rhs.max_value);
float lhs_scale = forward_exp2_sm90(
(lhs.max_value - result.max_value) * kForwardLog2E);
float rhs_scale = forward_exp2_sm90(
(rhs.max_value - result.max_value) * kForwardLog2E);
result.exp_sum = lhs.exp_sum * lhs_scale + rhs.exp_sum * rhs_scale;
if constexpr (ReturnEntropy) {
result.exp_weighted_sum =
lhs.exp_weighted_sum * lhs_scale +
rhs.exp_weighted_sum * rhs_scale;
}
// The reference accumulates the target logit additively: at most one
// contributor is non-zero because the shard is partitioned.
result.target_logit = lhs.target_logit + rhs.target_logit;
result.has_target = lhs.has_target || rhs.has_target;
return result;
}
template <bool ReturnEntropy>
__host__ __device__ static void store_partial(
const OnlineSoftmaxState& state,
const ForwardGemmPartialsSm90<Compute>& partials,
int index) {
partials.partial_max[index] = state.max_value;
partials.partial_sum[index] = state.exp_sum;
partials.partial_target[index] = state.target_logit;
if constexpr (ReturnEntropy) {
partials.partial_weighted[index] = state.exp_weighted_sum;
}
}
template <bool ReturnEntropy>
__host__ __device__ static void store_row(
const OnlineSoftmaxState& state,
const ForwardLocalStatsBuffers& output,
int row) {
output.local_max[row] = state.max_value;
output.local_sum[row] = state.exp_sum;
output.local_target[row] = state.has_target ? state.target_logit : 0.0f;
if constexpr (ReturnEntropy) {
output.local_weighted_sum[row] = state.exp_weighted_sum;
}
}
};
// ───────────────────────────────────────────────────────────────────────────
// Host-side launch geometry. CuTe-free so the launcher declaration, the split
// resolution and the shared-memory budget stay visible without CUTLASS.
// ───────────────────────────────────────────────────────────────────────────
template <int Compute = 90>
struct ForwardGemmLaunchSm90 {
using Config = ForwardGemmConfigSm90<Compute>;
static constexpr int kCompute = Compute;
static_assert(Compute == 90,
"SM90 fused scaled linear cross entropy requires Compute=90");
__host__ __device__ static constexpr int num_m_tiles(int tokens) {
return ceil_div(tokens, Config::kTileM);
}
__host__ __device__ static constexpr int num_m_pairs(int tokens) {
return ceil_div(num_m_tiles(tokens), Config::kClusterM);
}
__host__ __device__ static constexpr int num_k_tiles(int hidden) {
return ceil_div(hidden, Config::kTileK);
}
__host__ __device__ static constexpr int num_logical_n_tiles(
int local_vocab) {
return ceil_div(local_vocab, Config::kLogicalTileN);
}
// _resolve_split. `max_active_cluster_pairs` is the device's occupancy for
// a CLUSTER_M cluster (cutlass HardwareInfo.get_max_active_clusters).
__host__ static ForwardGemmSplitSm90<Compute> resolve_split(
const ForwardGemmTuningSm90<Compute>& tuning,
int tokens,
int local_vocab,
int max_active_cluster_pairs) {
ForwardGemmSplitSm90<Compute> split;
split.num_m_tiles = num_m_tiles(tokens);
split.num_m_pairs = num_m_pairs(tokens);
split.num_logical_n_tiles = num_logical_n_tiles(local_vocab);
if (tuning.base_split_n != 0) {
split.split_n = tuning.split_n;
split.base_split_n = tuning.base_split_n;
split.extra_m_pairs = tuning.extra_m_pairs;
} else if (tuning.split_n != 0) {
split.split_n = tuning.split_n;
split.base_split_n = tuning.split_n;
split.extra_m_pairs = 0;
} else {
int max_split_n = tuning.max_split_n > 0
? tuning.max_split_n
: Config::kDefaultMaxSplitN;
int target_cluster_pairs = tuning.target_cluster_pairs > 0
? tuning.target_cluster_pairs
: (max_active_cluster_pairs > 0 ? max_active_cluster_pairs : 1);
int per_pair_cap = max_split_n < split.num_logical_n_tiles
? max_split_n
: split.num_logical_n_tiles;
int max_cluster_pairs = split.num_m_pairs * per_pair_cap;
int wanted = target_cluster_pairs < max_cluster_pairs
? target_cluster_pairs
: max_cluster_pairs;
int cluster_pairs =
split.num_m_pairs > wanted ? split.num_m_pairs : wanted;
split.base_split_n = cluster_pairs / split.num_m_pairs;
split.extra_m_pairs = cluster_pairs % split.num_m_pairs;
split.split_n =
split.base_split_n + (split.extra_m_pairs != 0 ? 1 : 0);
}
split.num_cluster_pairs =
split.num_m_pairs * split.base_split_n + split.extra_m_pairs;
return split;
}
// Upper bound on the split-partial scratch, independent of the runtime
// split resolution so callers can size it ahead of the launch.
__host__ __device__ static std::size_t workspace_bytes(
int tokens,
int local_vocab,
bool return_entropy,
int max_split_n = Config::kDefaultMaxSplitN) {
int logical_n = num_logical_n_tiles(local_vocab);
if (logical_n < 1) logical_n = 1;
int cap = max_split_n < logical_n ? max_split_n : logical_n;
if (cap < 1) cap = 1;
std::size_t rows = static_cast<std::size_t>(num_m_tiles(tokens)) *
static_cast<std::size_t>(cap) *
static_cast<std::size_t>(Config::kTileM);
return rows * sizeof(float) * (return_entropy ? 4u : 3u);
}
// Mirrors ForwardGemmSmemSm90<Compute, ReturnEntropy> — duplicated so
// callers can validate the dynamic allocation without CUTLASS.
__host__ __device__ static constexpr int smem_bytes(bool return_entropy) {
return
// X + two N160 weight panels, BF16, kMainloopStages deep.
(Config::kTileM + Config::kLogicalTileN) * Config::kTileK * 2 *
Config::kMainloopStages +
// Two padded FP16 N80 staging buffers.
Config::kLogitBuffers * Config::kTileM * Config::kLogitStride * 2 +
// Cross-segment (max, sum, target) exchange.
3 * Config::kTileM * 4 +
// Optional weighted-entropy moment exchange.
(return_entropy ? Config::kTileM : 1) * 4 +
// TMA mbarrier pairs.
16 * Config::kMainloopStages +
// Struct alignment padding (four 1024 B aligned regions).
4 * 1024;
}
static_assert(smem_bytes(true) <= kHopperMaxSmemBytes,
"SM90 forward shared-memory footprint exceeds the Hopper limit");
};
// End-to-end tensor-parallel forward: the exact CuTe-DSL local-statistic GEMM
// followed by NCCL MAX, correction/packing, NCCL SUM, and finalization. This
// is the sole production forward host launcher; it directly prepares and
// launches forward_gemm_kernel_sm90.
//
// `gemm` describes this rank's contiguous vocabulary shard. Its `output` and
// `workspace` fields are launcher-owned and overwritten: both the local
// statistics and the vocabulary-split partials are internal scratch that never
// escapes the operation, so they come from liger::global_buffer_pool() rather
// than from the caller. Call configure_forward_tp_workspace() first (see
// forward_reduce.cuh).
template <int Compute = 90>
struct ForwardTpParamsSm90 {
static constexpr int kCompute = Compute;
ForwardGemmParamsSm90<Compute> gemm = {};
float* nll = nullptr; // [tokens]
float* lse = nullptr; // [tokens]
float* entropy = nullptr; // [tokens], required when ReturnEntropy=true
// ncclComm_t for the tensor-parallel communicator, as an int64 handle.
std::int64_t nccl_comm_handle = 0;
};
template <bool ReturnEntropy, int Compute = 90>
void fused_linear_scaled_cross_entropy_forward(
const ForwardTpParamsSm90<Compute>& params,
cudaStream_t stream);
extern template void fused_linear_scaled_cross_entropy_forward<false, 90>(
const ForwardTpParamsSm90<90>&,
cudaStream_t);
extern template void fused_linear_scaled_cross_entropy_forward<true, 90>(
const ForwardTpParamsSm90<90>&,
cudaStream_t);
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,414 @@
// ═══════════════════════════════════════════════════════════════════════════
// Internal SM90 forward GEMM self test.
//
// Compiled out unless LIGER_FSLCE_FORWARD_SM90_SELFTEST is defined, so the
// core library's recursive .cu glob picks this file up as an empty translation
// unit and no shared CMake/test file has to change. Build it standalone with:
//
// nvcc -std=c++17 -arch=sm_90a -O3 --expt-relaxed-constexpr \
// -DLIGER_FSLCE_FORWARD_SM90_SELFTEST \
// forward_gemm_sm90_selftest.cu \
// -I<this dir> -I<core include> -I$CUTLASS_HOME/include -o selftest
//
// Checks the fused kernel's local (max, sum, lse, target, weighted) statistics
// against a CPU reference that reproduces the same staging precision: the
// reference rounds every logit through FP16 exactly like the N80 SMEM slices,
// then applies the temperature scaling.
// ═══════════════════════════════════════════════════════════════════════════
#if defined(LIGER_FSLCE_FORWARD_SM90_SELFTEST)
#include "forward_gemm_mainloop_sm90.cuh"
#include "forward_gemm_sm90.cuh"
#include <cuda_runtime.h>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
namespace {
namespace fslce = liger::fused_scaled_linear_cross_entropy;
using Element = cutlass::bfloat16_t;
using ElementLogit = cutlass::half_t;
int g_failures = 0;
#define CUDA_OK(expr) \
do { \
cudaError_t status = (expr); \
if (status != cudaSuccess) { \
std::printf("CUDA error %s at %s:%d: %s\n", \
#expr, __FILE__, __LINE__, cudaGetErrorString(status)); \
std::exit(1); \
} \
} while (0)
float next_random(std::uint64_t& state) {
state = state * 6364136223846793005ULL + 1442695040888963407ULL;
std::uint32_t bits = static_cast<std::uint32_t>(state >> 33);
return static_cast<float>(bits % 2048) / 1024.0f - 1.0f;
}
struct ReferenceRow {
float max_value;
float exp_sum;
float lse;
float target_logit;
float weighted_sum;
};
// CPU mirror of the device epilogue: FP32 dot product, FP16 staging round,
// temperature scaling, then a plain (non-online) softmax reduction.
std::vector<ReferenceRow> reference(
const std::vector<Element>& x,
const std::vector<Element>& weight,
const std::vector<std::int64_t>& target,
int tokens,
int hidden,
int local_vocab,
std::int64_t vocab_start,
std::int64_t ignore_index,
float inverse_temperature) {
std::vector<ReferenceRow> rows(tokens);
std::vector<float> scaled(local_vocab);
for (int m = 0; m < tokens; ++m) {
float row_max = -1.0e38f;
for (int v = 0; v < local_vocab; ++v) {
float dot = 0.0f;
for (int k = 0; k < hidden; ++k) {
dot += static_cast<float>(x[m * hidden + k]) *
static_cast<float>(weight[v * hidden + k]);
}
float staged = static_cast<float>(static_cast<ElementLogit>(dot));
scaled[v] = staged * inverse_temperature;
row_max = fmaxf(row_max, scaled[v]);
}
double sum = 0.0;
double weighted = 0.0;
for (int v = 0; v < local_vocab; ++v) {
double e = std::exp(
static_cast<double>(scaled[v]) - static_cast<double>(row_max));
sum += e;
weighted += e * static_cast<double>(scaled[v]);
}
float target_logit = 0.0f;
std::int64_t target_id = target[m];
if (target_id != ignore_index) {
std::int64_t local = target_id - vocab_start;
if (local >= 0 && local < local_vocab) {
target_logit = scaled[static_cast<int>(local)];
}
}
rows[m].max_value = row_max;
rows[m].exp_sum = static_cast<float>(sum);
rows[m].lse = row_max + static_cast<float>(std::log(sum));
rows[m].target_logit = target_logit;
rows[m].weighted_sum = static_cast<float>(weighted);
}
return rows;
}
void expect_near(
const char* label,
int row,
float actual,
float expected,
float absolute,
float relative) {
float tolerance = absolute + relative * fabsf(expected);
if (!(fabsf(actual - expected) <= tolerance)) {
std::printf(
" FAIL %-14s row %4d: got %14.6f want %14.6f (tol %.6f)\n",
label, row, actual, expected, tolerance);
++g_failures;
}
}
// Test-only direct kernel launch. Production has no GEMM-only host wrapper:
// fused_linear_scaled_cross_entropy_forward owns this setup.
template <bool ReturnEntropy>
void launch_forward_gemm_selftest(
const fslce::ForwardGemmParamsSm90<90>& params,
cudaStream_t stream) {
using Traits = fslce::ForwardGemmTraitsSm90<90>;
using Config = typename Traits::Config;
using Launch = fslce::ForwardGemmLaunchSm90<90>;
using Smem = fslce::ForwardGemmSmemSm90<90, ReturnEntropy>;
using Cluster = liger::sm90::ClusterLaunchSm90<90>;
auto tma_load_x = liger::sm90::TmaSm90<90>::template make_load<
typename Traits::SmemLayoutX1,
Traits::kTileM,
Traits::kTileK>(
static_cast<const Element*>(params.x),
params.tokens,
params.hidden);
auto tma_load_w =
liger::sm90::TmaSm90<90>::template make_load_multicast<
typename Traits::SmemLayoutW1,
Traits::kAccumulatorTileN,
Traits::kTileK,
Traits::kClusterM>(
static_cast<const Element*>(params.weight),
params.local_vocab,
params.hidden);
auto* kernel_fn = &fslce::forward_gemm_kernel_sm90<
ReturnEntropy, 90, decltype(tma_load_x), decltype(tma_load_w)>;
constexpr int kSmemBytes = static_cast<int>(sizeof(Smem));
CUDA_OK(Cluster::prepare(kernel_fn, kSmemBytes));
int max_active_cluster_pairs = Cluster::max_active_clusters(
kernel_fn, Config::kNumThreads, kSmemBytes, Config::kClusterM);
fslce::ForwardGemmSplitSm90<90> split = Launch::resolve_split(
params.tuning,
params.tokens,
params.local_vocab,
max_active_cluster_pairs);
std::size_t partial_rows =
static_cast<std::size_t>(split.num_m_tiles) *
static_cast<std::size_t>(split.split_n) *
static_cast<std::size_t>(Config::kTileM);
float* scratch = static_cast<float*>(params.workspace);
fslce::ForwardGemmPartialsSm90<90> partials;
partials.partial_max = scratch + 0 * partial_rows;
partials.partial_sum = scratch + 1 * partial_rows;
partials.partial_target = scratch + 2 * partial_rows;
partials.partial_weighted =
ReturnEntropy ? scratch + 3 * partial_rows : nullptr;
CUDA_OK(Cluster::launch(
kernel_fn,
dim3(
static_cast<unsigned>(Config::kClusterM),
1u,
static_cast<unsigned>(split.num_cluster_pairs)),
Config::kNumThreads,
kSmemBytes,
Config::kClusterM,
stream,
tma_load_x,
tma_load_w,
params,
partials,
split));
fslce::forward_split_reduce_kernel_sm90<ReturnEntropy, 90>
<<<split.num_m_tiles, Config::kTileM, 0, stream>>>(
params, partials, split);
}
template <bool ReturnEntropy>
void run_case(
const std::string& name,
int tokens,
int hidden,
int local_vocab,
std::int64_t vocab_start,
float temperature,
std::uint64_t seed,
fslce::ForwardGemmTuningSm90<90> tuning = {}) {
std::uint64_t state = seed;
std::vector<Element> host_x(static_cast<std::size_t>(tokens) * hidden);
std::vector<Element> host_w(static_cast<std::size_t>(local_vocab) * hidden);
std::vector<std::int64_t> host_target(tokens);
const float scale = 1.0f / std::sqrt(static_cast<float>(hidden));
for (auto& value : host_x) {
value = static_cast<Element>(next_random(state) * scale * 4.0f);
}
for (auto& value : host_w) {
value = static_cast<Element>(next_random(state) * scale * 4.0f);
}
const std::int64_t global_vocab = vocab_start + local_vocab + 37;
for (int m = 0; m < tokens; ++m) {
if (m % 11 == 3) {
host_target[m] = -100; // ignore_index
} else if (m % 5 == 0) {
// Force a target that lands outside this vocabulary shard.
host_target[m] = (vocab_start + local_vocab) % global_vocab;
} else {
std::uint64_t bits = state = state * 2862933555777941757ULL + 3037000493ULL;
host_target[m] = vocab_start +
static_cast<std::int64_t>(bits % static_cast<std::uint64_t>(local_vocab));
}
}
Element* device_x = nullptr;
Element* device_w = nullptr;
std::int64_t* device_target = nullptr;
CUDA_OK(cudaMalloc(&device_x, host_x.size() * sizeof(Element)));
CUDA_OK(cudaMalloc(&device_w, host_w.size() * sizeof(Element)));
CUDA_OK(cudaMalloc(&device_target, host_target.size() * sizeof(std::int64_t)));
CUDA_OK(cudaMemcpy(device_x, host_x.data(),
host_x.size() * sizeof(Element), cudaMemcpyHostToDevice));
CUDA_OK(cudaMemcpy(device_w, host_w.data(),
host_w.size() * sizeof(Element), cudaMemcpyHostToDevice));
CUDA_OK(cudaMemcpy(device_target, host_target.data(),
host_target.size() * sizeof(std::int64_t), cudaMemcpyHostToDevice));
float* device_stats = nullptr;
CUDA_OK(cudaMalloc(&device_stats, 4 * tokens * sizeof(float)));
CUDA_OK(cudaMemset(device_stats, 0xff, 4 * tokens * sizeof(float)));
using Launch = fslce::ForwardGemmLaunchSm90<90>;
std::size_t workspace_bytes = Launch::workspace_bytes(
tokens, local_vocab, ReturnEntropy, tuning.max_split_n);
void* device_workspace = nullptr;
CUDA_OK(cudaMalloc(&device_workspace, workspace_bytes));
fslce::ForwardGemmParamsSm90<90> params;
params.x = device_x;
params.weight = device_w;
params.target = device_target;
params.output.local_max = device_stats + 0 * tokens;
params.output.local_sum = device_stats + 1 * tokens;
params.output.local_target = device_stats + 2 * tokens;
params.output.local_weighted_sum = device_stats + 3 * tokens;
params.workspace = device_workspace;
params.workspace_bytes = workspace_bytes;
params.tokens = tokens;
params.hidden = hidden;
params.local_vocab = local_vocab;
params.vocab_start = vocab_start;
params.ignore_index = -100;
params.inverse_temperature = 1.0f / temperature;
params.tuning = tuning;
launch_forward_gemm_selftest<ReturnEntropy>(params, nullptr);
CUDA_OK(cudaGetLastError());
CUDA_OK(cudaDeviceSynchronize());
std::vector<float> host_stats(4 * tokens);
CUDA_OK(cudaMemcpy(host_stats.data(), device_stats,
host_stats.size() * sizeof(float), cudaMemcpyDeviceToHost));
std::vector<ReferenceRow> expected = reference(
host_x, host_w, host_target, tokens, hidden, local_vocab,
vocab_start, params.ignore_index, params.inverse_temperature);
int before = g_failures;
for (int m = 0; m < tokens; ++m) {
expect_near("local_max", m, host_stats[0 * tokens + m],
expected[m].max_value, 2.0e-3f, 1.0e-3f);
expect_near("local_sum", m, host_stats[1 * tokens + m],
expected[m].exp_sum, 1.0e-3f, 5.0e-3f);
expect_near("local_target", m, host_stats[2 * tokens + m],
expected[m].target_logit, 2.0e-3f, 1.0e-3f);
if constexpr (ReturnEntropy) {
expect_near("local_weighted", m, host_stats[3 * tokens + m],
expected[m].weighted_sum, 5.0e-3f, 5.0e-3f);
}
if (g_failures > before + 20) break;
}
std::printf("%-46s %s (M=%d H=%d Vloc=%d start=%lld T=%.2f entropy=%d "
"tuning split=%d base=%d extra=%d)\n",
name.c_str(),
g_failures == before ? "PASS" : "FAIL",
tokens, hidden, local_vocab,
static_cast<long long>(vocab_start),
temperature,
static_cast<int>(ReturnEntropy),
tuning.split_n, tuning.base_split_n, tuning.extra_m_pairs);
CUDA_OK(cudaFree(device_x));
CUDA_OK(cudaFree(device_w));
CUDA_OK(cudaFree(device_target));
CUDA_OK(cudaFree(device_stats));
CUDA_OK(cudaFree(device_workspace));
}
void report_occupancy() {
using Traits = fslce::ForwardGemmTraitsSm90<90>;
std::printf(
"geometry: threads=%d cluster_m=%d stages=%d tileM=%d tileK=%d "
"logicalN=%d accumN=%d sliceN=%d chunkN=%d tma_bytes=%d\n",
Traits::kNumThreads, Traits::kClusterM, Traits::kStages,
Traits::kTileM, Traits::kTileK, Traits::kLogicalTileN,
Traits::kAccumulatorTileN, Traits::kEpilogueSliceN,
Traits::kSoftmaxChunkN, Traits::kTmaTransBytes);
std::printf("smem: no-entropy=%zu B entropy=%zu B (Hopper limit %d B)\n",
sizeof(fslce::ForwardGemmSmemSm90<90, false>),
sizeof(fslce::ForwardGemmSmemSm90<90, true>),
fslce::kHopperMaxSmemBytes);
}
} // namespace
int main() {
int device = 0;
cudaDeviceProp properties = {};
CUDA_OK(cudaGetDeviceProperties(&properties, device));
if (properties.major != 9) {
std::printf("skipping: requires a Hopper (sm90) GPU, found sm_%d%d\n",
properties.major, properties.minor);
return 0;
}
report_occupancy();
try {
// Aligned: M and V are exact multiples of the tile geometry.
run_case<false>("aligned / no entropy", 256, 128, 640, 0, 1.0f, 1);
run_case<true>("aligned / entropy", 256, 128, 640, 0, 1.0f, 2);
// Ragged M (odd M-tile count exercises the redundant cluster peer),
// ragged H (TMA zero fill in K) and ragged V (epilogue masking).
// H stays a multiple of 8 so the BF16 row stride keeps TMA's 16 B
// global-alignment requirement.
run_case<false>("ragged M/H/V / no entropy", 200, 104, 517, 0, 1.0f, 3);
run_case<true>("ragged M/H/V / entropy", 200, 104, 517, 0, 1.0f, 4);
// K tail: 72 = one full K64 tile plus 8 valid columns.
run_case<true>("ragged K tail", 128, 72, 333, 0, 1.0f, 8);
// Sharded vocabulary with a non-zero start plus temperature scaling.
run_case<true>("shard offset + temperature", 384, 192, 901, 4096, 0.7f, 5);
run_case<false>("single M tile", 96, 64, 321, 12288, 1.3f, 6);
// Local vocabulary smaller than one logical N320 tile.
run_case<true>("tiny local vocabulary", 128, 64, 40, 100000, 1.0f, 7);
// Vocabulary-split scheduler. M=1024 -> 8 M tiles -> 4 M pairs;
// V=2560 -> 8 logical N320 tiles.
fslce::ForwardGemmTuningSm90<90> uniform;
uniform.split_n = 3;
uniform.base_split_n = 3;
uniform.extra_m_pairs = 0;
run_case<true>("split: uniform base_split_n=3",
1024, 128, 2560, 0, 1.0f, 9, uniform);
// Uneven split: the first 3 of 4 M pairs carry an extra vocabulary
// split, so cluster_work crosses both scheduler branches.
fslce::ForwardGemmTuningSm90<90> uneven;
uneven.split_n = 3;
uneven.base_split_n = 2;
uneven.extra_m_pairs = 3;
run_case<true>("split: uneven base=2 extra=3",
1024, 128, 2560, 0, 1.0f, 10, uneven);
run_case<false>("split: uneven / no entropy",
1024, 128, 2560, 7168, 0.85f, 11, uneven);
// Max split with a ragged M tail on top of an uneven schedule.
fslce::ForwardGemmTuningSm90<90> ragged_split;
ragged_split.split_n = 2;
ragged_split.base_split_n = 1;
ragged_split.extra_m_pairs = 1;
run_case<true>("split: uneven + ragged M/V",
648, 136, 1613, 512, 1.0f, 12, ragged_split);
} catch (const std::exception& error) {
std::printf("exception: %s\n", error.what());
return 1;
}
std::printf("%s: %d comparison failures\n",
g_failures == 0 ? "OK" : "FAILED", g_failures);
return g_failures == 0 ? 0 : 1;
}
#endif // LIGER_FSLCE_FORWARD_SM90_SELFTEST
@@ -0,0 +1,132 @@
#include "forward_reduce.cuh"
#include <cstddef>
#include "buffer_pool.cuh"
#include "liger_cute/check.h"
namespace liger {
namespace fused_scaled_linear_cross_entropy {
namespace {
int g_capacity_tokens = 0;
std::size_t g_capacity_split_partials_bytes = 0;
std::size_t token_bytes_at(int tokens) {
return static_cast<std::size_t>(tokens) * sizeof(float);
}
std::size_t packed_bytes_at(int tokens) {
return static_cast<std::size_t>(kForwardReducedFields) *
token_bytes_at(tokens);
}
std::size_t split_partials_bytes_at(int tokens, int local_vocab) {
return ForwardGemmLaunchSm90<90>::workspace_bytes(
tokens, local_vocab, /*return_entropy=*/true);
}
void ensure_capacity(int tokens) {
LIGER_CHECK(tokens > 0, "tokens must be positive");
LIGER_CHECK(
g_capacity_tokens != 0,
"fused_scaled_linear_cross_entropy forward: call "
"configure_forward_tp_workspace(max_tokens, max_local_vocab) before "
"the first launch");
LIGER_CHECK(
tokens <= g_capacity_tokens,
"fused_scaled_linear_cross_entropy forward workspace was configured "
"for ",
g_capacity_tokens,
" tokens but ",
tokens,
" were requested");
}
} // namespace
void configure_forward_tp_workspace(int max_tokens, int max_local_vocab) {
LIGER_CHECK(max_tokens > 0, "max_tokens must be positive");
LIGER_CHECK(max_local_vocab > 0, "max_local_vocab must be positive");
std::size_t split_bytes =
split_partials_bytes_at(max_tokens, max_local_vocab);
if (max_tokens <= g_capacity_tokens &&
split_bytes <= g_capacity_split_partials_bytes) {
return;
}
LIGER_CHECK(
g_capacity_tokens == 0,
"fused_scaled_linear_cross_entropy forward workspace is immutable "
"after its first configuration");
using Names = ForwardBufferNames;
std::size_t token_bytes = token_bytes_at(max_tokens);
std::size_t packed_bytes = packed_bytes_at(max_tokens);
auto& pool = global_buffer_pool();
pool.get_device(Names::kLocalMax, token_bytes);
pool.get_device(Names::kGlobalMax, token_bytes);
pool.get_device(Names::kPacked, packed_bytes);
pool.get_device(Names::kReduced, packed_bytes);
pool.get_device(Names::kGemmSplitPartials, split_bytes);
pool.get_device(Names::kLocalSum, token_bytes);
pool.get_device(Names::kLocalTarget, token_bytes);
pool.get_device(Names::kLocalWeighted, token_bytes);
g_capacity_tokens = max_tokens;
g_capacity_split_partials_bytes = split_bytes;
}
std::size_t forward_tp_workspace_device_bytes(
int max_tokens, int max_local_vocab) {
LIGER_CHECK(max_tokens > 0, "max_tokens must be positive");
LIGER_CHECK(max_local_vocab > 0, "max_local_vocab must be positive");
return 5 * token_bytes_at(max_tokens) +
2 * packed_bytes_at(max_tokens) +
split_partials_bytes_at(max_tokens, max_local_vocab);
}
void reset_forward_tp_workspace_configuration() {
g_capacity_tokens = 0;
g_capacity_split_partials_bytes = 0;
}
template <bool ReturnEntropy>
ForwardTpWorkspace reserve_forward_tp_workspace(int tokens) {
ensure_capacity(tokens);
using Names = ForwardBufferNames;
std::size_t token_bytes = token_bytes_at(g_capacity_tokens);
std::size_t packed_bytes = packed_bytes_at(g_capacity_tokens);
auto& pool = global_buffer_pool();
ForwardTpWorkspace workspace = {};
workspace.local.local_max = static_cast<float*>(
pool.get_device(Names::kLocalMax, token_bytes));
workspace.global_max = static_cast<float*>(
pool.get_device(Names::kGlobalMax, token_bytes));
workspace.packed = static_cast<float*>(
pool.get_device(Names::kPacked, packed_bytes));
workspace.reduced = static_cast<float*>(
pool.get_device(Names::kReduced, packed_bytes));
workspace.gemm_split_partials = pool.get_device(
Names::kGemmSplitPartials, g_capacity_split_partials_bytes);
workspace.gemm_split_partials_bytes =
g_capacity_split_partials_bytes;
workspace.local.local_sum = static_cast<float*>(
pool.get_device(Names::kLocalSum, token_bytes));
workspace.local.local_target = static_cast<float*>(
pool.get_device(Names::kLocalTarget, token_bytes));
workspace.local.local_weighted_sum = static_cast<float*>(
pool.get_device(Names::kLocalWeighted, token_bytes));
workspace.tokens = tokens;
workspace.fields = forward_reduced_fields(ReturnEntropy);
return workspace;
}
template ForwardTpWorkspace reserve_forward_tp_workspace<false>(int);
template ForwardTpWorkspace reserve_forward_tp_workspace<true>(int);
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,80 @@
#pragma once
// Launcher-owned workspace for the tensor-parallel forward. The GEMM and
// split reducer produce rank-local FP32 statistics here; the host launcher then
// performs NCCL MAX/SUM all-reduces with small CUDA kernels between/after them.
// No forward buffer is remotely addressed by device code, so every allocation
// is ordinary device memory from the shared BufferPool.
#include <cuda_runtime.h>
#include <cstddef>
#include "forward_gemm_sm90.cuh"
namespace liger {
namespace fused_scaled_linear_cross_entropy {
inline constexpr int kForwardReducedSumField = 0;
inline constexpr int kForwardReducedTargetField = 1;
inline constexpr int kForwardReducedWeightedField = 2;
inline constexpr int kForwardReducedFields = 3;
struct ForwardBufferNames {
static constexpr const char* kLocalMax =
"fused_scaled_linear_cross_entropy_tp_forward_local_max";
static constexpr const char* kGlobalMax =
"fused_scaled_linear_cross_entropy_tp_forward_global_max";
static constexpr const char* kPacked =
"fused_scaled_linear_cross_entropy_tp_forward_packed";
static constexpr const char* kReduced =
"fused_scaled_linear_cross_entropy_tp_forward_reduced";
static constexpr const char* kGemmSplitPartials =
"fused_scaled_linear_cross_entropy_tp_forward_split_partials";
static constexpr const char* kLocalSum =
"fused_scaled_linear_cross_entropy_tp_forward_local_sum";
static constexpr const char* kLocalTarget =
"fused_scaled_linear_cross_entropy_tp_forward_local_target";
static constexpr const char* kLocalWeighted =
"fused_scaled_linear_cross_entropy_tp_forward_local_weighted";
};
__host__ __device__ constexpr int forward_reduced_fields(
bool return_entropy) {
return return_entropy
? kForwardReducedFields
: kForwardReducedFields - 1;
}
struct ForwardTpWorkspace {
ForwardLocalStatsBuffers local;
float* global_max;
float* packed;
float* reduced;
void* gemm_split_partials;
std::size_t gemm_split_partials_bytes;
int tokens;
int fields;
};
// Fixes reusable workspace capacities. Capacities are immutable after the
// first call; callers must configure their maximum shape upfront.
void configure_forward_tp_workspace(int max_tokens, int max_local_vocab);
std::size_t forward_tp_workspace_device_bytes(
int max_tokens,
int max_local_vocab);
// Invalidates cached capacities after the shared BufferPool is cleared.
void reset_forward_tp_workspace_configuration();
template <bool ReturnEntropy>
ForwardTpWorkspace reserve_forward_tp_workspace(int tokens);
extern template ForwardTpWorkspace
reserve_forward_tp_workspace<false>(int);
extern template ForwardTpWorkspace
reserve_forward_tp_workspace<true>(int);
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,427 @@
// ═══════════════════════════════════════════════════════════════════════════
// Internal multi-PE self test for the tensor-parallel forward path.
//
// Compiled out unless LIGER_FSLCE_FORWARD_TP_SM90_SELFTEST is defined, so the
// core library's recursive .cu glob picks this file up as an empty translation
// unit and no shared CMake/test file has to change. Build and run with:
//
// nvcc -std=c++17 -arch=sm_90a -O3 --expt-relaxed-constexpr \
// -c fused_linear_scaled_cross_entropy_forward.cu -o forward.o <includes>
// nvcc -std=c++17 -arch=sm_90a -O3 --expt-relaxed-constexpr -rdc=true \
// -c forward_reduce.cu -o forward_reduce.o <includes>
// nvcc -std=c++17 -arch=sm_90a -O3 --expt-relaxed-constexpr -rdc=true \
// -DLIGER_FSLCE_FORWARD_TP_SM90_SELFTEST \
// forward_tp_sm90_selftest.cu forward_reduce.o forward.o \
// -I<this dir> -I<core include> -I<core src/moe> \
// -I$CUTLASS_HOME/include -I$NVSHMEM_HOME/include \
// -L$NVSHMEM_HOME/lib -lnvshmem_host -lnvshmem_device -lcuda -o tp_selftest
// nvshmrun -n 4 ./tp_selftest
//
// Every PE owns a contiguous shard of one global vocabulary. Each PE rebuilds
// the *whole* weight matrix from the shared seed and checks the reduced
// NLL / LSE / entropy against a full-vocabulary CPU reference, so a wrong
// global max, a missed rescale or a dropped target contribution all show up.
// ═══════════════════════════════════════════════════════════════════════════
#if defined(LIGER_FSLCE_FORWARD_TP_SM90_SELFTEST)
#include <nvshmem.h>
#include <nvshmemx.h>
#include <nccl.h>
#include <cuda_runtime.h>
#include <cmath>
#include <cstring>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
#include "buffer_pool.cuh"
#include "forward_gemm_sm90.cuh"
#include "forward_reduce.cuh"
namespace {
namespace fslce = liger::fused_scaled_linear_cross_entropy;
using Element = __nv_bfloat16;
int g_pe = 0;
int g_n_pes = 1;
int g_failures = 0;
ncclComm_t g_nccl_comm = nullptr;
#define CUDA_OK(expr) \
do { \
cudaError_t status = (expr); \
if (status != cudaSuccess) { \
std::printf("[pe %d] CUDA error %s at %s:%d: %s\n", \
g_pe, #expr, __FILE__, __LINE__, \
cudaGetErrorString(status)); \
std::exit(1); \
} \
} while (0)
#define NCCL_OK(expr) \
do { \
ncclResult_t status = (expr); \
if (status != ncclSuccess) { \
std::printf("[pe %d] NCCL error %s at %s:%d: %s\n", \
g_pe, #expr, __FILE__, __LINE__, ncclGetErrorString(status)); \
std::exit(1); \
} \
} while (0)
ncclComm_t bootstrap_nccl() {
ncclUniqueId id;
std::memset(&id, 0, sizeof(id));
if (g_pe == 0) NCCL_OK(ncclGetUniqueId(&id));
void* source = nvshmem_malloc(sizeof(id));
void* destination = nvshmem_malloc(sizeof(id));
CUDA_OK(cudaMemcpy(source, &id, sizeof(id), cudaMemcpyHostToDevice));
nvshmem_barrier_all();
nvshmem_broadcastmem(
NVSHMEM_TEAM_WORLD,
destination,
source,
sizeof(id),
0);
nvshmem_barrier_all();
CUDA_OK(cudaMemcpy(
&id, destination, sizeof(id), cudaMemcpyDeviceToHost));
nvshmem_free(destination);
nvshmem_free(source);
ncclComm_t comm = nullptr;
NCCL_OK(ncclCommInitRank(&comm, g_n_pes, id, g_pe));
return comm;
}
float next_random(std::uint64_t& state) {
state = state * 6364136223846793005ULL + 1442695040888963407ULL;
std::uint32_t bits = static_cast<std::uint32_t>(state >> 33);
return static_cast<float>(bits % 2048) / 1024.0f - 1.0f;
}
// Deterministic from (seed, index) so every PE reconstructs the same global
// tensors without any communication.
float element_at(std::uint64_t seed, std::size_t index, float scale) {
std::uint64_t state = seed ^ (index * 0x9E3779B97F4A7C15ULL);
state = state * 6364136223846793005ULL + 1442695040888963407ULL;
state ^= state >> 29;
state = state * 0xBF58476D1CE4E5B9ULL;
state ^= state >> 32;
std::uint32_t bits = static_cast<std::uint32_t>(state >> 33);
return (static_cast<float>(bits % 2048) / 1024.0f - 1.0f) * scale;
}
struct ReferenceRow {
float lse;
float nll;
float entropy;
float row_max;
float sum;
float expectation;
};
// Full-vocabulary CPU reference, reproducing the kernel's FP16 staging round.
std::vector<ReferenceRow> reference(
const std::vector<float>& x,
const std::vector<float>& weight_global,
const std::vector<std::int64_t>& target,
int tokens,
int hidden,
int global_vocab,
std::int64_t ignore_index,
float inverse_temperature) {
std::vector<ReferenceRow> rows(tokens);
std::vector<float> scaled(global_vocab);
for (int m = 0; m < tokens; ++m) {
float row_max = -1.0e38f;
for (int v = 0; v < global_vocab; ++v) {
float dot = 0.0f;
for (int k = 0; k < hidden; ++k) {
dot += x[static_cast<std::size_t>(m) * hidden + k] *
weight_global[static_cast<std::size_t>(v) * hidden + k];
}
scaled[v] = __half2float(__float2half(dot)) * inverse_temperature;
row_max = fmaxf(row_max, scaled[v]);
}
double sum = 0.0;
double weighted = 0.0;
for (int v = 0; v < global_vocab; ++v) {
double e = std::exp(
static_cast<double>(scaled[v]) - static_cast<double>(row_max));
sum += e;
weighted += e * static_cast<double>(scaled[v]);
}
float lse = row_max + static_cast<float>(std::log(sum));
bool ignored = target[m] == ignore_index;
rows[m].expectation = static_cast<float>(weighted / sum);
rows[m].row_max = row_max;
rows[m].sum = static_cast<float>(sum);
rows[m].lse = lse;
rows[m].nll = ignored ? 0.0f : lse - scaled[target[m]];
rows[m].entropy = ignored
? 0.0f
: lse - static_cast<float>(weighted / sum);
}
return rows;
}
void expect_near(
const char* label, int row, float actual, float expected,
float absolute, float relative) {
float tolerance = absolute + relative * fabsf(expected);
if (!(fabsf(actual - expected) <= tolerance)) {
if (g_failures < 20) {
std::printf(
"[pe %d] FAIL %-8s row %4d: got %14.6f want %14.6f "
"(tol %.6f)\n",
g_pe, label, row, actual, expected, tolerance);
}
++g_failures;
}
}
template <bool ReturnEntropy>
void run_case(
const std::string& name,
int tokens,
int hidden,
int local_vocab,
float temperature,
std::uint64_t seed) {
const int global_vocab = local_vocab * g_n_pes;
const std::int64_t vocab_start =
static_cast<std::int64_t>(g_pe) * local_vocab;
const float scale = 4.0f / std::sqrt(static_cast<float>(hidden));
// Rebuild the global tensors identically on every PE.
std::vector<float> host_x(static_cast<std::size_t>(tokens) * hidden);
for (std::size_t i = 0; i < host_x.size(); ++i) {
host_x[i] = element_at(seed, i, scale);
}
std::vector<float> host_w(
static_cast<std::size_t>(global_vocab) * hidden);
for (std::size_t i = 0; i < host_w.size(); ++i) {
host_w[i] = element_at(seed ^ 0xABCDEF01ULL, i, scale);
}
// Round through BF16 so host and device see identical operands.
for (auto& v : host_x) v = __bfloat162float(__float2bfloat16(v));
for (auto& v : host_w) v = __bfloat162float(__float2bfloat16(v));
std::vector<std::int64_t> host_target(tokens);
std::uint64_t target_state = seed ^ 0x5DEECE66DULL;
for (int m = 0; m < tokens; ++m) {
if (m % 13 == 5) {
host_target[m] = -100; // ignore_index
} else {
next_random(target_state);
host_target[m] = static_cast<std::int64_t>(
(target_state >> 17) % static_cast<std::uint64_t>(global_vocab));
}
}
// Device operands: X is replicated, W is this PE's contiguous shard.
std::vector<Element> device_x_host(host_x.size());
for (std::size_t i = 0; i < host_x.size(); ++i) {
device_x_host[i] = __float2bfloat16(host_x[i]);
}
std::vector<Element> device_w_host(
static_cast<std::size_t>(local_vocab) * hidden);
for (std::size_t i = 0; i < device_w_host.size(); ++i) {
device_w_host[i] = __float2bfloat16(
host_w[static_cast<std::size_t>(vocab_start) * hidden + i]);
}
Element* device_x = nullptr;
Element* device_w = nullptr;
std::int64_t* device_target = nullptr;
CUDA_OK(cudaMalloc(&device_x, device_x_host.size() * sizeof(Element)));
CUDA_OK(cudaMalloc(&device_w, device_w_host.size() * sizeof(Element)));
CUDA_OK(cudaMalloc(&device_target, tokens * sizeof(std::int64_t)));
CUDA_OK(cudaMemcpy(device_x, device_x_host.data(),
device_x_host.size() * sizeof(Element), cudaMemcpyHostToDevice));
CUDA_OK(cudaMemcpy(device_w, device_w_host.data(),
device_w_host.size() * sizeof(Element), cudaMemcpyHostToDevice));
CUDA_OK(cudaMemcpy(device_target, host_target.data(),
tokens * sizeof(std::int64_t), cudaMemcpyHostToDevice));
float* device_out = nullptr;
CUDA_OK(cudaMalloc(&device_out, 3 * tokens * sizeof(float)));
CUDA_OK(cudaMemset(device_out, 0xff, 3 * tokens * sizeof(float)));
fslce::ForwardTpParamsSm90<90> params;
params.gemm.x = device_x;
params.gemm.weight = device_w;
params.gemm.target = device_target;
params.gemm.tokens = tokens;
params.gemm.hidden = hidden;
params.gemm.local_vocab = local_vocab;
params.gemm.vocab_start = vocab_start;
params.gemm.ignore_index = -100;
params.gemm.inverse_temperature = 1.0f / temperature;
params.nll = device_out + 0 * tokens;
params.lse = device_out + 1 * tokens;
params.entropy = device_out + 2 * tokens;
params.nccl_comm_handle =
reinterpret_cast<std::int64_t>(g_nccl_comm);
fslce::fused_linear_scaled_cross_entropy_forward<
ReturnEntropy, 90>(params, nullptr);
CUDA_OK(cudaGetLastError());
CUDA_OK(cudaDeviceSynchronize());
std::vector<float> host_out(3 * tokens);
CUDA_OK(cudaMemcpy(host_out.data(), device_out,
host_out.size() * sizeof(float), cudaMemcpyDeviceToHost));
fslce::ForwardTpWorkspace probe =
fslce::reserve_forward_tp_workspace<ReturnEntropy>(tokens);
std::vector<float> host_global_max(tokens);
std::vector<float> host_reduced(
static_cast<std::size_t>(probe.fields) * tokens);
CUDA_OK(cudaMemcpy(host_global_max.data(), probe.global_max,
tokens * sizeof(float), cudaMemcpyDeviceToHost));
CUDA_OK(cudaMemcpy(host_reduced.data(), probe.reduced,
host_reduced.size() * sizeof(float), cudaMemcpyDeviceToHost));
std::vector<ReferenceRow> expected = reference(
host_x, host_w, host_target, tokens, hidden, global_vocab,
params.gemm.ignore_index, params.gemm.inverse_temperature);
int before = g_failures;
int stage_max_bad = 0;
int stage_sum_bad = 0;
for (int m = 0; m < tokens; ++m) {
if (fabsf(host_global_max[m] - expected[m].row_max) >
2.0e-3f + 1.0e-3f * fabsf(expected[m].row_max)) {
++stage_max_bad;
}
float reduced_sum = host_reduced[0 * tokens + m];
if (fabsf(reduced_sum - expected[m].sum) >
1.0e-2f + 5.0e-3f * fabsf(expected[m].sum)) {
++stage_sum_bad;
}
}
if (stage_max_bad || stage_sum_bad) {
std::printf("[pe %d] STAGE %-28s max_bad=%d sum_bad=%d of %d rows\n",
g_pe, name.c_str(), stage_max_bad, stage_sum_bad, tokens);
}
for (int m = 0; m < tokens; ++m) {
expect_near("lse", m, host_out[1 * tokens + m],
expected[m].lse, 3.0e-3f, 2.0e-3f);
expect_near("nll", m, host_out[0 * tokens + m],
expected[m].nll, 3.0e-3f, 2.0e-3f);
if constexpr (ReturnEntropy) {
expect_near("entropy", m, host_out[2 * tokens + m],
expected[m].entropy, 5.0e-3f, 5.0e-3f);
// Backward recovers the softmax-weighted mean logit from the two
// saved output tensors as (lse - entropy); check that identity
// holds wherever the gradient is non-zero.
if (host_target[m] != params.gemm.ignore_index) {
expect_near("lse-entropy", m,
host_out[1 * tokens + m] - host_out[2 * tokens + m],
expected[m].expectation, 5.0e-3f, 5.0e-3f);
}
}
if (g_failures > before + 20) break;
}
if (g_pe == 0) {
std::printf("%-40s %s (PEs=%d M=%d H=%d Vloc=%d Vglobal=%d T=%.2f "
"entropy=%d)\n",
name.c_str(),
g_failures == before ? "PASS" : "FAIL",
g_n_pes, tokens, hidden, local_vocab, global_vocab, temperature,
static_cast<int>(ReturnEntropy));
}
// Keep every PE on the same case boundary before pooled symmetric
// reduction buffers are reused by the next launch.
nvshmem_barrier_all();
CUDA_OK(cudaFree(device_x));
CUDA_OK(cudaFree(device_w));
CUDA_OK(cudaFree(device_target));
CUDA_OK(cudaFree(device_out));
}
} // namespace
int main() {
nvshmem_init();
g_pe = nvshmem_my_pe();
g_n_pes = nvshmem_n_pes();
CUDA_OK(cudaSetDevice(nvshmem_team_my_pe(NVSHMEMX_TEAM_NODE)));
cudaDeviceProp properties = {};
CUDA_OK(cudaGetDeviceProperties(&properties, nvshmem_team_my_pe(
NVSHMEMX_TEAM_NODE)));
if (properties.major != 9) {
if (g_pe == 0) {
std::printf("skipping: requires Hopper (sm90), found sm_%d%d\n",
properties.major, properties.minor);
}
nvshmem_finalize();
return 0;
}
if (g_pe == 0) {
std::printf("tensor-parallel forward over %d PEs\n", g_n_pes);
}
g_nccl_comm = bootstrap_nccl();
// Capacities are immutable once allocated, so configure for the largest
// shape used below before any launch.
constexpr int kMaxTokens = 1024;
constexpr int kMaxLocalVocab = 2560;
fslce::configure_forward_tp_workspace(kMaxTokens, kMaxLocalVocab);
try {
run_case<false>("tp aligned / no entropy", 256, 128, 640, 1.0f, 101);
run_case<true>("tp aligned / entropy", 256, 128, 640, 1.0f, 102);
run_case<true>("tp ragged M/H/V", 200, 104, 517, 0.8f, 103);
run_case<false>("tp temperature / no entropy", 384, 192, 901, 1.4f, 104);
run_case<true>("tp large split", 1024, 128, 2560, 1.0f, 105);
} catch (const std::exception& error) {
std::printf("[pe %d] exception: %s\n", g_pe, error.what());
ncclCommDestroy(g_nccl_comm);
nvshmem_finalize();
return 1;
}
// Make every rank's verdict visible before PE 0 prints the summary.
// Pooled, not raw nvshmem_malloc: this module never runs its own symmetric
// allocation lifecycle.
auto& pool = liger::global_buffer_pool();
int* local_slot = static_cast<int*>(
pool.get_symmetric("fslce_tp_selftest_verdict_local", sizeof(int)));
int* total_slot = static_cast<int*>(
pool.get_symmetric("fslce_tp_selftest_verdict_total", sizeof(int)));
CUDA_OK(cudaMemcpy(local_slot, &g_failures, sizeof(int),
cudaMemcpyHostToDevice));
nvshmem_int_sum_reduce(NVSHMEM_TEAM_WORLD, total_slot, local_slot, 1);
int host_total = 0;
CUDA_OK(cudaMemcpy(&host_total, total_slot, sizeof(int),
cudaMemcpyDeviceToHost));
if (g_pe == 0) {
std::printf("%s: %d comparison failures across %d PEs\n",
host_total == 0 ? "OK" : "FAILED", host_total, g_n_pes);
}
NCCL_OK(ncclCommDestroy(g_nccl_comm));
fslce::reset_forward_tp_workspace_configuration();
liger::global_buffer_pool().clear();
nvshmem_finalize();
return host_total == 0 ? 0 : 1;
}
#endif // LIGER_FSLCE_FORWARD_TP_SM90_SELFTEST
@@ -0,0 +1,542 @@
// ═══════════════════════════════════════════════════════════════════════════
// SM90 fused scaled linear cross entropy backward host dispatch. dZ always
// selects the zero-spill cluster-2 handoff. NVLS and hierarchical paths use
// the four-stage combined dX+dW cluster kernel; direct-peer uses its fallback.
// ═══════════════════════════════════════════════════════════════════════════
// Must precede every other CUTLASS/CuTe include: it opts the TU into CuTe's
// extended SM90 MMA shapes.
#include "backward_gemm_mainloop_sm90.cuh"
#include "backward_gemm_sm90.cuh"
#include "backward_reduce_sm90.cuh"
#include "gemm_sm90.cuh"
#include "liger_cute/check.h"
#include "liger_cute/detail/tp_reduce.cuh"
#include "workspace.cuh"
#include <cstddef>
#include <cstdint>
namespace liger {
namespace fused_scaled_linear_cross_entropy {
namespace {
using namespace cute;
using HostConfig = BackwardGemmConfigSm90<90>;
using HostLaunch = BackwardGemmLaunchSm90<90>;
using HostTraits = BackwardGemmTraitsSm90<90>;
using HostCombinedDwTraits = BackwardGemmTraitsSm90<90, 4, 64, 2>;
using HostDxClusterTraits = BackwardDxCluster2TraitsSm90<90>;
using HostDzHandoffTraits = BackwardDzHandoffTraitsSm90<90>;
using HostElement = HostTraits::Element;
// The staging ring depth comes from dx_reduce.cuh so the kernel template,
// symmetric capacity and CTA-local mbarrier arrays cannot drift apart.
void check_cuda(cudaError_t error, const char* what) {
LIGER_CHECK(
error == cudaSuccess,
"fused_scaled_linear_cross_entropy backward: ",
what,
" failed: ",
cudaGetErrorString(error));
}
template <class Layout, class Element>
auto make_row_major_load(const Element* ptr, int64_t rows, int64_t cols) {
return make_tma_copy(
SM90_TMA_LOAD{},
backward_row_major_tensor(ptr, rows, cols),
Layout{});
}
template <class Layout, int TileRows, int TileCols, class Element>
auto make_row_major_load_tile(
const Element* ptr, int64_t rows, int64_t cols) {
return make_tma_copy(
SM90_TMA_LOAD{},
backward_row_major_tensor(ptr, rows, cols),
Layout{},
make_shape(Int<TileRows>{}, Int<TileCols>{}),
_1{});
}
template <class Layout, class Element>
auto make_row_major_store(Element* ptr, int64_t rows, int64_t cols) {
return make_tma_copy(
SM90_TMA_STORE{},
backward_row_major_tensor(ptr, rows, cols),
Layout{});
}
template <class Layout, class Element>
auto make_row_major_reduce_add(Element* ptr, int64_t rows, int64_t cols) {
return make_tma_copy(
SM90_TMA_REDUCE_ADD{},
backward_row_major_tensor(ptr, rows, cols),
Layout{});
}
template <class Layout, int TileRows, int TileCols, class Element>
auto make_row_major_store_tile(Element* ptr, int64_t rows, int64_t cols) {
return make_tma_copy(
SM90_TMA_STORE{},
backward_row_major_tensor(ptr, rows, cols),
Layout{},
make_shape(Int<TileRows>{}, Int<TileCols>{}),
_1{});
}
template <class Layout, class Element>
auto make_col_major_load(
const Element* ptr, int64_t rows, int64_t cols, int64_t leading) {
return make_tma_copy(
SM90_TMA_LOAD{},
backward_col_major_tensor(ptr, rows, cols, leading),
Layout{});
}
template <
class Layout,
int TileRows,
int TileCols,
int Multicast,
class Element>
auto make_col_major_load_multicast(
const Element* ptr, int64_t rows, int64_t cols, int64_t leading) {
return make_tma_copy(
SM90_TMA_LOAD_MULTICAST{},
backward_col_major_tensor(ptr, rows, cols, leading),
Layout{},
make_shape(Int<TileRows>{}, Int<TileCols>{}),
Int<Multicast>{});
}
void validate(const BackwardTpParamsSm90<90>& params) {
const auto& gemm = params.gemm;
LIGER_CHECK(gemm.tokens >= 0, "tokens must be non-negative");
LIGER_CHECK(gemm.hidden > 0, "hidden must be positive");
LIGER_CHECK(gemm.local_vocab > 0, "local_vocab must be positive");
LIGER_CHECK(
gemm.x != nullptr && gemm.weight != nullptr &&
gemm.target != nullptr && gemm.grad_output != nullptr &&
gemm.lse != nullptr,
"backward inputs x / weight / target / grad_output / lse must be "
"non-null");
LIGER_CHECK(
gemm.grad_input != nullptr && gemm.grad_weight != nullptr,
"backward outputs grad_input and grad_weight must be non-null");
LIGER_CHECK(
gemm.dz_workspace != nullptr,
"the dZ wave workspace must be reserved");
LIGER_CHECK(
params.tiles_per_reduce == 1 || params.tiles_per_reduce == 2 ||
params.tiles_per_reduce == 4,
"TilesPerReduce must be 1, 2 or 4, got ",
params.tiles_per_reduce);
LIGER_CHECK(
params.num_comm_channels >= 1,
"num_comm_channels must be positive (legacy compatibility argument)");
// TMA needs 16 B aligned base addresses and row strides for its BF16
// operands. dX uses generic 8 B vector stores.
auto aligned = [](const void* pointer) {
return reinterpret_cast<std::uintptr_t>(pointer) % 16 == 0;
};
LIGER_CHECK(
aligned(gemm.x) && aligned(gemm.weight) &&
aligned(gemm.grad_weight) && aligned(gemm.dz_workspace),
"backward TMA BF16 operands must be 16 B aligned");
LIGER_CHECK(
reinterpret_cast<std::uintptr_t>(gemm.grad_input) % 8 == 0,
"grad_input must be 8 B aligned for vectorized BF16 stores");
LIGER_CHECK(
(static_cast<std::size_t>(gemm.hidden) * sizeof(HostElement)) % 16 == 0,
"hidden * sizeof(bfloat16) must be a multiple of 16 B for TMA");
validate_backward_tp_shape(
gemm.tokens, gemm.hidden, gemm.local_vocab);
std::size_t needed = HostLaunch::dz_workspace_bytes(gemm.local_vocab);
LIGER_CHECK(
gemm.dz_workspace_bytes >= needed,
"the dZ wave workspace needs ",
needed,
" B, got ",
gemm.dz_workspace_bytes);
}
template <
int TilesPerReduce,
bool ReturnEntropy,
liger_cute::detail::LocalReduceBackend Backend,
bool RequiresRemote>
void launch_instance(
const BackwardTpParamsSm90<90>& params,
const liger_cute::detail::TpReducePlan& reduce,
cudaStream_t stream) {
static constexpr bool kUseNvls =
Backend == liger_cute::detail::LocalReduceBackend::kNvls;
static_assert(
!RequiresRemote || kUseNvls,
"the remote follow-up requires a local NVLS reduce-scatter");
using CommConfig =
DxCommConfig<HostConfig, kDxRingStages, TilesPerReduce, 90>;
using DzSmem = BackwardDzHandoffSmemSm90<90, ReturnEntropy>;
static constexpr bool kUseCluster = kUseNvls;
const auto& gemm = params.gemm;
int padded_vocab = HostLaunch::padded_vocab(gemm.local_vocab);
auto* x = static_cast<const HostElement*>(gemm.x);
auto* w = static_cast<const HostElement*>(gemm.weight);
auto* dz = static_cast<HostElement*>(gemm.dz_workspace);
auto* dw_out = static_cast<HostElement*>(gemm.grad_weight);
DxReduceWorkspace<float> comm = {};
int resident_ctas = backward_dx_resident_cta_capacity();
constexpr int kClusterStoreRows =
kMaxDxResidentCtas * kDxCommWarpsPerChannel *
kDxRingStages * TilesPerReduce * HostDxClusterTraits::kTileM;
int cluster_store_rows =
resident_ctas * kDxCommWarpsPerChannel *
kDxRingStages * TilesPerReduce * HostDxClusterTraits::kTileM;
using ClusterStoreTma = decltype(
make_row_major_store<
typename HostDxClusterTraits::SmemStore::Single>(
static_cast<float*>(nullptr),
kClusterStoreRows,
HostDxClusterTraits::kTileN));
ClusterStoreTma cluster_tma_store = {};
if constexpr (kUseCluster) {
LIGER_CHECK(
reduce.backend ==
liger_cute::detail::LocalReduceBackend::kNvls,
"fused_scaled_linear_cross_entropy backward: clustered dX "
"requires a node-local NVLS team");
LIGER_CHECK(
reduce.remote.enabled() == RequiresRemote,
"fused_scaled_linear_cross_entropy backward: remote reduction "
"dispatch does not match the configured plan");
comm = reserve_dx_reduce_workspace(
TilesPerReduce,
kDxRingStages,
resident_ctas);
cluster_tma_store =
make_row_major_store<
typename HostDxClusterTraits::SmemStore::Single>(
comm.partial,
cluster_store_rows,
HostDxClusterTraits::kTileN);
}
auto cluster_tma_dz =
make_row_major_load<typename HostDxClusterTraits::SmemA::Single>(
dz,
HostConfig::kWaveRows,
padded_vocab);
auto cluster_tma_wt = make_col_major_load_multicast<
typename HostDxClusterTraits::SmemB::Single,
HostDxClusterTraits::kFragmentTileN,
HostDxClusterTraits::kTileK,
HostDxClusterTraits::kClusterM>(
w,
gemm.hidden,
gemm.local_vocab,
gemm.hidden);
auto cluster_tma_dzt = make_col_major_load_multicast<
typename HostCombinedDwTraits::SmemAMnMajor::Single,
HostCombinedDwTraits::kTileM,
HostCombinedDwTraits::kTileK,
HostDxClusterTraits::kClusterM>(
dz,
padded_vocab,
HostConfig::kWaveRows,
padded_vocab);
auto cluster_tma_xt = make_col_major_load<
typename HostCombinedDwTraits::SmemBMnMajor::Single>(
x, gemm.hidden, gemm.tokens, gemm.hidden);
auto cluster_tma_dw_store = make_row_major_store<
typename HostCombinedDwTraits::DwSmemStore1::Single>(
dw_out, gemm.local_vocab, gemm.hidden);
auto cluster_tma_dw_add = make_row_major_reduce_add<
typename HostCombinedDwTraits::DwSmemStore1::Single>(
dw_out, gemm.local_vocab, gemm.hidden);
using ClusterBundle = BackwardClusterCombinedTmaBundleSm90<
decltype(cluster_tma_dz),
decltype(cluster_tma_wt),
ClusterStoreTma,
decltype(cluster_tma_dzt),
decltype(cluster_tma_xt),
decltype(cluster_tma_dw_store),
decltype(cluster_tma_dw_add)>;
ClusterBundle cluster_bundle{
cluster_tma_dz,
cluster_tma_wt,
cluster_tma_store,
cluster_tma_dzt,
cluster_tma_xt,
cluster_tma_dw_store,
cluster_tma_dw_add};
const bool use_split_k2 =
backward_dx_split_k(gemm.hidden, gemm.local_vocab) == 2;
auto* cluster_kernel = use_split_k2
? &backward_dx_cluster2_gemm_wave_kernel_sm90<
2, 90, CommConfig, ClusterBundle>
: &backward_dx_cluster2_gemm_wave_kernel_sm90<
1, 90, CommConfig, ClusterBundle>;
auto tma_x =
make_row_major_load_tile<
typename HostDzHandoffTraits::SmemTraitsA::Single,
HostDzHandoffTraits::kTileM,
HostDzHandoffTraits::kTileK>(
x, gemm.tokens, gemm.hidden);
auto tma_w =
make_row_major_load_tile<
typename HostDzHandoffTraits::SmemTraitsB::Single,
HostDzHandoffTraits::kTileN,
HostDzHandoffTraits::kTileK>(
w, gemm.local_vocab, gemm.hidden);
auto tma_dz_store =
make_row_major_store_tile<
typename HostDzHandoffTraits::SmemStoreSingle,
HostDzHandoffTraits::kTileM,
HostDzHandoffTraits::kStoreTileN>(
dz, HostConfig::kWaveRows, padded_vocab);
auto* dz_kernel_fn = &backward_dz_handoff_wave_kernel_sm90<
ReturnEntropy,
90,
decltype(tma_x),
decltype(tma_w),
decltype(tma_dz_store)>;
constexpr int kDzSmemBytes = static_cast<int>(sizeof(DzSmem));
static_assert(kDzSmemBytes <= kBackwardMaxSmemBytes,
"SM90 backward dZ shared-memory footprint exceeds the Hopper limit");
using DzCluster = sm90::ClusterLaunchSm90<90>;
check_cuda(
DzCluster::prepare(dz_kernel_fn, kDzSmemBytes),
"cudaFuncSetAttribute(dZ MaxDynamicSharedMemorySize / "
"NonPortableClusterSizeAllowed)");
using DxCluster = sm90::ClusterLaunchSm90<90>;
constexpr int kClusterSmemBytes =
static_cast<int>(sizeof(BackwardDxCluster2SmemSm90<90>));
static_assert(kClusterSmemBytes == 230400,
"production combined dX+dW shared memory must remain 230400 B");
static_assert(kClusterSmemBytes <= kBackwardMaxSmemBytes,
"SM90 dX cluster2 shared-memory footprint exceeds Hopper limit");
static_assert(HostConfig::kMTilesPerWave %
HostDxClusterTraits::kClusterM == 0);
int dx_cluster_count = 0;
if constexpr (kUseCluster) {
check_cuda(
DxCluster::prepare(cluster_kernel, kClusterSmemBytes),
"cudaFuncSetAttribute(dX cluster2 MaxDynamicSharedMemorySize / "
"NonPortableClusterSizeAllowed)");
int max_active_clusters = DxCluster::max_active_clusters(
cluster_kernel,
HostDxClusterTraits::kNumThreads,
kClusterSmemBytes,
HostDxClusterTraits::kClusterM);
LIGER_CHECK(
max_active_clusters > 0,
"fused_scaled_linear_cross_entropy backward: "
"no active dX cluster2 fits");
int total_cluster_tiles =
(HostConfig::kMTilesPerWave /
HostDxClusterTraits::kClusterM) *
HostLaunch::num_dx_n_tiles(gemm.hidden) *
(use_split_k2 ? 2 : 1);
int dw_tiles = HostLaunch::num_dw_m_tiles(gemm.local_vocab) *
HostLaunch::num_dw_n_tiles(gemm.hidden);
int combined_tiles = total_cluster_tiles > ceil_div(dw_tiles, 2)
? total_cluster_tiles
: ceil_div(dw_tiles, 2);
dx_cluster_count =
combined_tiles < max_active_clusters
? combined_tiles
: max_active_clusters;
LIGER_CHECK(
dx_cluster_count * HostDxClusterTraits::kClusterM <=
resident_ctas,
"fused_scaled_linear_cross_entropy backward: dX cluster grid "
"exceeds the configured resident CTA capacity");
}
BackwardGemmParamsSm90<90> device_params = gemm;
int dz_work = HostConfig::kMTilesPerWave * ceil_div(
padded_vocab,
HostDzHandoffTraits::kClusterM *
HostDzHandoffTraits::kTileN);
int max_dz_clusters = DzCluster::max_active_clusters(
dz_kernel_fn,
HostConfig::kNumThreads,
kDzSmemBytes,
HostDzHandoffTraits::kClusterM);
LIGER_CHECK(
max_dz_clusters > 0,
"fused_scaled_linear_cross_entropy backward: "
"no active dZ handoff clusters fit");
int dz_clusters =
dz_work < max_dz_clusters ? dz_work : max_dz_clusters;
int dx_groups_per_wave = HostConfig::kMTilesPerWave *
ceil_div(HostLaunch::num_dx_n_tiles(gemm.hidden), TilesPerReduce);
int dw_total = HostLaunch::num_dw_m_tiles(gemm.local_vocab) *
HostLaunch::num_dw_n_tiles(gemm.hidden);
int work = dx_groups_per_wave;
if (dw_total > work) work = dw_total;
int grid = work < resident_ctas ? work : resident_ctas;
int num_waves = HostLaunch::num_waves(gemm.tokens);
if constexpr (!kUseCluster) {
comm = reserve_dx_reduce_workspace(
TilesPerReduce, kDxRingStages, grid);
}
liger_cute::detail::begin_tp_reduce(comm.launch_epoch, stream);
if constexpr (kUseCluster) {
if (use_split_k2) {
std::size_t durable_elements =
static_cast<std::size_t>(num_waves) *
HostConfig::kWaveRows *
static_cast<std::size_t>(
HostLaunch::num_dx_n_tiles(gemm.hidden)) *
HostDxClusterTraits::kTileN /
static_cast<std::size_t>(
reduce.nvls.size);
check_cuda(
cudaMemsetAsync(
reduce.nvls.reduced_shard,
0,
durable_elements * sizeof(float),
stream),
"clear split-K dX durable output");
}
}
for (int wave = 0; wave < num_waves; ++wave) {
check_cuda(
DzCluster::launch(
dz_kernel_fn,
dim3(
HostDzHandoffTraits::kClusterM,
1,
static_cast<unsigned>(dz_clusters)),
HostConfig::kNumThreads,
kDzSmemBytes,
HostDzHandoffTraits::kClusterM,
stream,
tma_x,
tma_w,
tma_dz_store,
device_params,
wave),
"cudaLaunchKernelEx(backward_dz_handoff_wave_kernel_sm90)");
if constexpr (kUseCluster) {
check_cuda(
DxCluster::launch(
cluster_kernel,
dim3(
HostDxClusterTraits::kClusterM,
1,
static_cast<unsigned>(dx_cluster_count)),
HostDxClusterTraits::kNumThreads,
kClusterSmemBytes,
HostDxClusterTraits::kClusterM,
stream,
cluster_bundle,
comm,
reduce.nvls,
device_params,
wave),
"cudaLaunchKernelEx("
"backward_dx_cluster2_gemm_wave_kernel_sm90)");
} else {
launch_backward_dx_dw_direct_peer_wave_sm90(
ReturnEntropy,
TilesPerReduce,
device_params,
comm,
reduce.direct,
grid,
stream,
wave);
}
}
if constexpr (kUseCluster) {
int cluster_grid =
dx_cluster_count * HostDxClusterTraits::kClusterM;
finalize_backward_dx_cluster_sm90<RequiresRemote>(
device_params,
comm,
reduce.nvls,
reduce.remote,
cluster_grid,
num_waves,
stream);
}
liger_cute::detail::end_tp_reduce(stream);
}
template <int TilesPerReduce, bool ReturnEntropy>
void dispatch_instance(
const BackwardTpParamsSm90<90>& params,
const liger_cute::detail::TpReducePlan& reduce,
cudaStream_t stream) {
if (reduce.backend ==
liger_cute::detail::LocalReduceBackend::kNvls) {
if (reduce.remote.enabled()) {
launch_instance<
TilesPerReduce,
ReturnEntropy,
liger_cute::detail::LocalReduceBackend::kNvls,
true>(params, reduce, stream);
} else {
launch_instance<
TilesPerReduce,
ReturnEntropy,
liger_cute::detail::LocalReduceBackend::kNvls,
false>(params, reduce, stream);
}
} else {
launch_instance<
TilesPerReduce,
ReturnEntropy,
liger_cute::detail::LocalReduceBackend::kDirectPeer,
false>(params, reduce, stream);
}
}
} // namespace
template <bool ReturnEntropy, int Compute>
void fused_linear_scaled_cross_entropy_backward(
const BackwardTpParamsSm90<Compute>& params, cudaStream_t stream) {
static_assert(Compute == 90,
"SM90 fused scaled linear cross entropy requires Compute=90");
if (params.gemm.tokens == 0) return;
validate(params);
liger_cute::detail::TpReducePlan reduce =
liger_cute::detail::tp_reduce_plan();
switch (params.tiles_per_reduce) {
case 1:
dispatch_instance<1, ReturnEntropy>(params, reduce, stream);
break;
case 2:
dispatch_instance<2, ReturnEntropy>(params, reduce, stream);
break;
default:
dispatch_instance<4, ReturnEntropy>(params, reduce, stream);
break;
}
}
template void fused_linear_scaled_cross_entropy_backward<false, 90>(
const BackwardTpParamsSm90<90>&,
cudaStream_t);
template void fused_linear_scaled_cross_entropy_backward<true, 90>(
const BackwardTpParamsSm90<90>&,
cudaStream_t);
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,348 @@
// ═══════════════════════════════════════════════════════════════════════════
// SM90 tensor-parallel fused scaled linear cross entropy — forward launcher.
//
// Sole production host call graph:
//
// fused_linear_scaled_cross_entropy_forward
// -> forward_gemm_kernel_sm90
// -> forward_split_reduce_kernel_sm90
// -> NCCL MAX
// -> pack_forward_reduction_kernel
// -> NCCL SUM
// -> finalize_forward_kernel
//
// The WGMMA translation unit is deliberately non-RDC. NVSHMEM collective
// kernels remain in forward_reduce.cu, which is compiled with RDC.
// ═══════════════════════════════════════════════════════════════════════════
// Must precede every other CUTLASS/CuTe include: it opts the TU into CuTe's
// extended SM90 MMA shapes, which is where the N160 WGMMA atom lives.
#include "forward_gemm_mainloop_sm90.cuh"
#include "forward_gemm_sm90.cuh"
#include "forward_reduce.cuh"
#include "gemm_sm90.cuh"
#include "liger_cute/check.h"
#include "online_softmax.cuh"
#include <nccl.h>
#include <cstddef>
#include <cstdint>
namespace liger {
namespace fused_scaled_linear_cross_entropy {
namespace {
template <bool ReturnEntropy>
__global__ void pack_forward_reduction_kernel(
const float* local_max,
const float* global_max,
const float* local_sum,
const float* local_target,
const float* local_weighted_sum,
float* packed,
int tokens) {
int row = static_cast<int>(blockIdx.x * blockDim.x + threadIdx.x);
if (row >= tokens) return;
float correction = local_sum[row] == 0.0f
? 0.0f
: expf(local_max[row] - global_max[row]);
packed[kForwardReducedSumField * tokens + row] =
local_sum[row] * correction;
packed[kForwardReducedTargetField * tokens + row] =
local_target[row];
if constexpr (ReturnEntropy) {
packed[kForwardReducedWeightedField * tokens + row] =
local_weighted_sum[row] * correction;
}
}
template <bool ReturnEntropy>
__global__ void finalize_forward_kernel(
const float* global_max,
const float* reduced,
const std::int64_t* target,
float* nll,
float* lse,
float* entropy,
int tokens,
std::int64_t ignore_index) {
int row = static_cast<int>(blockIdx.x * blockDim.x + threadIdx.x);
if (row >= tokens) return;
float global_weighted = 0.0f;
if constexpr (ReturnEntropy) {
global_weighted =
reduced[kForwardReducedWeightedField * tokens + row];
}
FinalizedSoftmax result = finalize_softmax<ReturnEntropy>(
global_max[row],
reduced[kForwardReducedSumField * tokens + row],
reduced[kForwardReducedTargetField * tokens + row],
global_weighted,
target[row] == ignore_index);
nll[row] = result.nll;
lse[row] = result.lse;
if constexpr (ReturnEntropy) {
entropy[row] = result.entropy;
}
}
void check_cuda(cudaError_t error, const char* what) {
LIGER_CHECK(
error == cudaSuccess,
"fused_scaled_linear_cross_entropy forward: ",
what,
" failed: ",
cudaGetErrorString(error));
}
void check_nccl(ncclResult_t result, const char* what) {
LIGER_CHECK(
result == ncclSuccess,
"fused_scaled_linear_cross_entropy forward: ",
what,
" failed: ",
ncclGetErrorString(result));
}
} // namespace
template <bool ReturnEntropy, int Compute>
void fused_linear_scaled_cross_entropy_forward(
const ForwardTpParamsSm90<Compute>& params,
cudaStream_t stream) {
using Traits = ForwardGemmTraitsSm90<Compute>;
using Config = typename Traits::Config;
using Launch = ForwardGemmLaunchSm90<Compute>;
using Element = typename Traits::Element;
using Smem = ForwardGemmSmemSm90<Compute, ReturnEntropy>;
static_assert(Compute == 90,
"SM90 fused scaled linear cross entropy requires Compute=90");
const ForwardGemmParamsSm90<Compute>& input = params.gemm;
LIGER_CHECK(input.tokens >= 0, "tokens must be non-negative");
if (input.tokens == 0) return;
LIGER_CHECK(input.hidden > 0, "hidden must be positive");
LIGER_CHECK(input.local_vocab > 0, "local_vocab must be positive");
LIGER_CHECK(
input.x != nullptr && input.weight != nullptr &&
input.target != nullptr,
"forward inputs must be non-null");
LIGER_CHECK(
params.nll != nullptr && params.lse != nullptr,
"forward TP outputs nll and lse must be non-null");
if constexpr (ReturnEntropy) {
LIGER_CHECK(
params.entropy != nullptr,
"entropy output is required when ReturnEntropy=true");
}
LIGER_CHECK(
params.nccl_comm_handle != 0,
"forward TP requires a valid ncclComm_t handle");
LIGER_CHECK(
reinterpret_cast<std::uintptr_t>(input.x) % 16 == 0 &&
reinterpret_cast<std::uintptr_t>(input.weight) % 16 == 0,
"forward GEMM operands must be 16 B aligned for TMA");
LIGER_CHECK(
(static_cast<std::size_t>(input.hidden) * sizeof(Element)) % 16 == 0,
"hidden * sizeof(bfloat16) must be a multiple of 16 B for TMA");
LIGER_CHECK(
input.tuning.split_n >= 0 && input.tuning.base_split_n >= 0 &&
input.tuning.extra_m_pairs >= 0 &&
input.tuning.target_cluster_pairs >= 0,
"split and cluster tuning values must be non-negative");
LIGER_CHECK(input.tuning.max_split_n >= 1, "max_split_n must be positive");
LIGER_CHECK(
input.tuning.extra_m_pairs == 0 ||
input.tuning.base_split_n != 0,
"extra_m_pairs requires an explicit base_split_n");
ForwardTpWorkspace reduce_workspace =
reserve_forward_tp_workspace<ReturnEntropy>(input.tokens);
ForwardGemmParamsSm90<Compute> gemm_params = input;
gemm_params.output = reduce_workspace.local;
gemm_params.workspace = reduce_workspace.gemm_split_partials;
gemm_params.workspace_bytes =
reduce_workspace.gemm_split_partials_bytes;
LIGER_CHECK(
gemm_params.output.local_max != nullptr &&
gemm_params.output.local_sum != nullptr &&
gemm_params.output.local_target != nullptr,
"forward GEMM local statistics buffers must be non-null");
if constexpr (ReturnEntropy) {
LIGER_CHECK(
gemm_params.output.local_weighted_sum != nullptr,
"local_weighted_sum is required when ReturnEntropy=true");
}
// Exact CuTe-DSL operand ordering: X is an ordinary TMA load and each
// N160 weight panel is multicast over the cluster's M mode.
auto tma_load_x = sm90::TmaSm90<Compute>::template make_load<
typename Traits::SmemLayoutX1,
Traits::kTileM,
Traits::kTileK>(
static_cast<const Element*>(gemm_params.x),
gemm_params.tokens,
gemm_params.hidden);
auto tma_load_w =
sm90::TmaSm90<Compute>::template make_load_multicast<
typename Traits::SmemLayoutW1,
Traits::kAccumulatorTileN,
Traits::kTileK,
Traits::kClusterM>(
static_cast<const Element*>(gemm_params.weight),
gemm_params.local_vocab,
gemm_params.hidden);
auto* kernel_fn = &forward_gemm_kernel_sm90<
ReturnEntropy, Compute, decltype(tma_load_x), decltype(tma_load_w)>;
constexpr int kSmemBytes = static_cast<int>(sizeof(Smem));
static_assert(kSmemBytes <= kHopperMaxSmemBytes,
"SM90 forward shared-memory footprint exceeds the Hopper limit");
static_assert(kSmemBytes <= Launch::smem_bytes(ReturnEntropy),
"ForwardGemmLaunchSm90::smem_bytes must bound the real footprint");
using Cluster = sm90::ClusterLaunchSm90<Compute>;
check_cuda(
Cluster::prepare(kernel_fn, kSmemBytes),
"cudaFuncSetAttribute(MaxDynamicSharedMemorySize / "
"NonPortableClusterSizeAllowed)");
int max_active_cluster_pairs = Cluster::max_active_clusters(
kernel_fn, Config::kNumThreads, kSmemBytes, Config::kClusterM);
ForwardGemmSplitSm90<Compute> split = Launch::resolve_split(
gemm_params.tuning,
gemm_params.tokens,
gemm_params.local_vocab,
max_active_cluster_pairs);
LIGER_CHECK(
split.base_split_n >= 1 &&
split.split_n <= split.num_logical_n_tiles,
"split_n must be between 1 and the number of logical vocabulary tiles");
LIGER_CHECK(
split.extra_m_pairs < split.num_m_pairs &&
split.split_n ==
split.base_split_n + (split.extra_m_pairs != 0 ? 1 : 0),
"uneven split tuning must use split_n=base_split_n+1 for a proper "
"subset of M pairs");
// Split partials are laid out [statistic][M tile][split][row].
std::size_t partial_rows =
static_cast<std::size_t>(split.num_m_tiles) *
static_cast<std::size_t>(split.split_n) *
static_cast<std::size_t>(Config::kTileM);
std::size_t partial_bytes = partial_rows * sizeof(float);
std::size_t required_bytes =
partial_bytes * (ReturnEntropy ? 4u : 3u);
LIGER_CHECK(
gemm_params.workspace != nullptr &&
gemm_params.workspace_bytes >= required_bytes,
"forward GEMM needs a ",
required_bytes,
" B split-partial workspace, got ",
gemm_params.workspace_bytes);
LIGER_CHECK(
reinterpret_cast<std::uintptr_t>(gemm_params.workspace) % 16 == 0,
"forward GEMM workspace must be 16 B aligned");
float* scratch = static_cast<float*>(gemm_params.workspace);
ForwardGemmPartialsSm90<Compute> partials;
partials.partial_max = scratch + 0 * partial_rows;
partials.partial_sum = scratch + 1 * partial_rows;
partials.partial_target = scratch + 2 * partial_rows;
partials.partial_weighted =
ReturnEntropy ? scratch + 3 * partial_rows : nullptr;
check_cuda(
Cluster::launch(
kernel_fn,
dim3(
static_cast<unsigned>(Config::kClusterM),
1u,
static_cast<unsigned>(split.num_cluster_pairs)),
Config::kNumThreads,
kSmemBytes,
Config::kClusterM,
stream,
tma_load_x,
tma_load_w,
gemm_params,
partials,
split),
"cudaLaunchKernelEx(forward_gemm_kernel_sm90)");
forward_split_reduce_kernel_sm90<ReturnEntropy, Compute>
<<<split.num_m_tiles, Config::kTileM, 0, stream>>>(
gemm_params, partials, split);
check_cuda(
cudaGetLastError(), "forward_split_reduce_kernel_sm90 launch");
ncclComm_t comm =
reinterpret_cast<ncclComm_t>(params.nccl_comm_handle);
check_nccl(
ncclAllReduce(
reduce_workspace.local.local_max,
reduce_workspace.global_max,
gemm_params.tokens,
ncclFloat32,
ncclMax,
comm,
stream),
"ncclAllReduce(MAX)");
constexpr int kPostThreads = 256;
int post_blocks = ceil_div(gemm_params.tokens, kPostThreads);
pack_forward_reduction_kernel<ReturnEntropy>
<<<post_blocks, kPostThreads, 0, stream>>>(
reduce_workspace.local.local_max,
reduce_workspace.global_max,
reduce_workspace.local.local_sum,
reduce_workspace.local.local_target,
reduce_workspace.local.local_weighted_sum,
reduce_workspace.packed,
gemm_params.tokens);
check_cuda(
cudaGetLastError(), "pack_forward_reduction_kernel launch");
check_nccl(
ncclAllReduce(
reduce_workspace.packed,
reduce_workspace.reduced,
static_cast<std::size_t>(reduce_workspace.fields) *
static_cast<std::size_t>(gemm_params.tokens),
ncclFloat32,
ncclSum,
comm,
stream),
"ncclAllReduce(SUM)");
finalize_forward_kernel<ReturnEntropy>
<<<post_blocks, kPostThreads, 0, stream>>>(
reduce_workspace.global_max,
reduce_workspace.reduced,
gemm_params.target,
params.nll,
params.lse,
params.entropy,
gemm_params.tokens,
gemm_params.ignore_index);
check_cuda(
cudaGetLastError(), "finalize_forward_kernel launch");
}
template void fused_linear_scaled_cross_entropy_forward<false, 90>(
const ForwardTpParamsSm90<90>&,
cudaStream_t);
template void fused_linear_scaled_cross_entropy_forward<true, 90>(
const ForwardTpParamsSm90<90>&,
cudaStream_t);
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,10 @@
#pragma once
#include "backward.cuh"
#include "backward_gemm_sm90.cuh"
#include "config.cuh"
#include "dx_reduce.cuh"
#include "forward_gemm_sm90.cuh"
#include "forward_reduce.cuh"
#include "online_softmax.cuh"
#include "workspace.cuh"
@@ -0,0 +1,506 @@
#pragma once
// ═══════════════════════════════════════════════════════════════════════════
// Reusable SM90 (Hopper) warp-specialized GEMM mechanics.
//
// Architecture-specific building blocks shared by the fused scaled linear
// cross entropy forward path and the (upcoming) backward dZ/dX/dW paths, so
// neither has to copy/paste TMA descriptor construction, SMEM swizzle atom
// selection, WGMMA atom selection, mbarrier pipeline wiring, warp-group
// register reconfiguration or the cluster launch dance.
//
// Design rules:
// * Zero-overhead: every entity is a template or a CUTE_DEVICE
// (__device__ __forceinline__) function, and the mainloop drivers take
// the caller's issue/fence steps as inlined functors. The generated SASS
// is bit-identical to the hand-written loops these replaced — see the
// cuobjdump diff in the module's validation notes.
// * Nothing here decides *ordering policy*. MainloopKLoopSm90 encodes the
// one-stage release lag the CuTe-DSL kernels rely on and nothing else;
// what to issue per stage stays with the caller.
// * `int Compute` on every GEMM-related template, defaulted to 90, with a
// static_assert so a future SM100 port is a new header, not a silent
// mis-instantiation of this one.
// ═══════════════════════════════════════════════════════════════════════════
// The extended SM90 MMA shapes (e.g. the N160 WGMMA atom) are opt-in in CuTe
// and must be enabled before the first CuTe include of the translation unit.
// Including this header first is what guarantees that.
#if !defined(CUTE_SM90_EXTENDED_MMA_SHAPES_ENABLED)
#define CUTE_SM90_EXTENDED_MMA_SHAPES_ENABLED
#endif
#include <cute/tensor.hpp>
#include <cute/algorithm/gemm.hpp>
#include <cute/arch/cluster_sm90.hpp>
#include <cute/atom/copy_traits_sm90_tma.hpp>
#include <cute/atom/mma_traits_sm90_gmma.hpp>
#include <cutlass/cutlass.h>
#include <cutlass/arch/reg_reconfig.h>
#include <cutlass/numeric_types.h>
#include <cutlass/pipeline/sm90_pipeline.hpp>
#include <cuda_runtime.h>
#include <cstdint>
namespace liger {
namespace sm90 {
using namespace cute;
// ───────────────────────────────────────────────────────────────────────────
// Architecture constants
// ───────────────────────────────────────────────────────────────────────────
template <int Compute = 90>
struct ArchSm90 {
static_assert(Compute == 90, "ArchSm90 requires Compute=90");
static constexpr int kCompute = Compute;
static constexpr int kWarpSize = 32;
static constexpr int kWarpsPerWarpGroup = 4;
static constexpr int kWarpGroupSize = kWarpSize * kWarpsPerWarpGroup;
// Hopper's opt-in dynamic shared-memory ceiling.
static constexpr int kMaxDynamicSmemBytes = 227 * 1024;
// A CTA may own at most 64K registers; ptxas therefore caps a 384-thread
// warp-specialized CTA at 168 registers per thread.
static constexpr int kRegisterFilePerCta = 65536;
};
// ───────────────────────────────────────────────────────────────────────────
// WGMMA atom / TiledMMA selection
// ───────────────────────────────────────────────────────────────────────────
// The M64 SS WGMMA atom for the requested N. Wraps CuTe's selector so callers
// do not have to know which shapes live in the extended set.
template <
class ElementA,
class ElementB,
class ElementAccum,
int AtomTileN,
GMMA::Major MajorA = GMMA::Major::K,
GMMA::Major MajorB = GMMA::Major::K,
int Compute = 90>
struct GmmaAtomSm90 {
static_assert(Compute == 90, "GmmaAtomSm90 requires Compute=90");
static constexpr int kCompute = Compute;
static constexpr int kAtomTileM = 64;
static constexpr int kAtomTileN = AtomTileN;
static constexpr int kAtomTileK = 16;
using Atom = decltype(cute::GMMA::ss_op_selector<
ElementA,
ElementB,
ElementAccum,
Shape<Int<kAtomTileM>, Int<AtomTileN>, Int<kAtomTileK>>,
MajorA,
MajorB>());
};
// Cooperative TiledMMA: NumWarpGroups warp groups split the tile along M, so
// each warp group owns an (AtomTileM, AtomTileN) accumulator.
template <
class ElementA,
class ElementB,
class ElementAccum,
int AtomTileN,
int NumWarpGroups,
GMMA::Major MajorA = GMMA::Major::K,
GMMA::Major MajorB = GMMA::Major::K,
int Compute = 90>
struct TiledMmaMSplitSm90 {
static_assert(Compute == 90, "TiledMmaMSplitSm90 requires Compute=90");
static constexpr int kCompute = Compute;
static constexpr int kNumWarpGroups = NumWarpGroups;
using AtomTraits = GmmaAtomSm90<
ElementA, ElementB, ElementAccum, AtomTileN, MajorA, MajorB, Compute>;
using Atom = typename AtomTraits::Atom;
using Type = TiledMMA<
MMA_Atom<Atom>, Layout<Shape<Int<NumWarpGroups>, _1, _1>>>;
static constexpr int kTileM = AtomTraits::kAtomTileM * NumWarpGroups;
static constexpr int kTileN = AtomTileN;
};
// ───────────────────────────────────────────────────────────────────────────
// SMEM layouts
// ───────────────────────────────────────────────────────────────────────────
// K-major operand staging with the 128 B swizzle atom, which is what a K tile
// of exactly 128 bytes requires.
template <class Element, int Rows, int Cols, int Stages, int Compute = 90>
struct SmemLayoutKMajorSm90 {
static_assert(Compute == 90, "SmemLayoutKMajorSm90 requires Compute=90");
static_assert(Cols * static_cast<int>(sizeof(Element)) % 128 == 0,
"the SW128 K-major atom needs 128 B rows");
static constexpr int kCompute = Compute;
using Atom = GMMA::Layout_K_SW128_Atom<Element>;
using Single = decltype(tile_to_shape(
Atom{}, Shape<Int<Rows>, Int<Cols>>{}));
using Staged = decltype(tile_to_shape(
Atom{}, Shape<Int<Rows>, Int<Cols>, Int<Stages>>{}));
static constexpr int kStageBytes = static_cast<int>(
cute::size(Single{}) * sizeof(Element));
static constexpr int kStagedElements = cosize_v<Staged>;
};
// ───────────────────────────────────────────────────────────────────────────
// TMA descriptors
// ───────────────────────────────────────────────────────────────────────────
template <int Compute = 90>
struct TmaSm90 {
static_assert(Compute == 90, "TmaSm90 requires Compute=90");
static constexpr int kCompute = Compute;
// int64 extents: rows * cols exceeds INT_MAX at production shapes and
// would otherwise wrap CuTe's layout algebra into a negative byte offset.
template <class Element>
static auto row_major_tensor(const Element* ptr, int rows, int cols) {
return make_tensor(
make_gmem_ptr(ptr),
make_layout(
make_shape(
static_cast<int64_t>(rows), static_cast<int64_t>(cols)),
make_stride(static_cast<int64_t>(cols), _1{})));
}
template <class SmemLayout, int TileRows, int TileCols, class Element>
static auto make_load(const Element* ptr, int rows, int cols) {
return make_tma_copy(
SM90_TMA_LOAD{},
row_major_tensor(ptr, rows, cols),
SmemLayout{},
make_shape(Int<TileRows>{}, Int<TileCols>{}),
_1{});
}
// Multicast splits the box across `Multicast` CTAs of the cluster; each
// peer issues its own slice and every peer receives the whole box, so the
// stage's transaction byte count stays the full tile.
template <
class SmemLayout, int TileRows, int TileCols, int Multicast,
class Element>
static auto make_load_multicast(const Element* ptr, int rows, int cols) {
return make_tma_copy(
SM90_TMA_LOAD_MULTICAST{},
row_major_tensor(ptr, rows, cols),
SmemLayout{},
make_shape(Int<TileRows>{}, Int<TileCols>{}),
Int<Multicast>{});
}
};
// Multicast mask over the cluster's M mode: every CTA sharing this N tile.
template <class ClusterShape, int Compute = 90>
CUTE_DEVICE std::uint16_t cluster_multicast_mask_m_sm90() {
static_assert(Compute == 90,
"cluster_multicast_mask_m_sm90 requires Compute=90");
std::uint16_t mask = 0;
auto block_layout = Layout<ClusterShape>{};
CUTE_UNROLL
for (int m = 0; m < size<0>(block_layout); ++m) {
mask |= std::uint16_t(1) << block_layout(m, Int<0>{}, Int<0>{});
}
return mask;
}
// ───────────────────────────────────────────────────────────────────────────
// Mainloop pipeline
// ───────────────────────────────────────────────────────────────────────────
enum class PipelineRoleSm90 : std::uint8_t {
kProducer,
kConsumer,
kNonParticipant,
};
template <int Stages, int Compute = 90>
struct MainloopPipelineSm90 {
static_assert(Compute == 90, "MainloopPipelineSm90 requires Compute=90");
static constexpr int kCompute = Compute;
static constexpr int kStages = Stages;
using Type = cutlass::PipelineTmaAsync<Stages>;
using State = cutlass::PipelineState<Stages>;
using SharedStorage = typename Type::SharedStorage;
using Category = typename Type::ThreadCategory;
// Role-specific construction. Barrier initialisation is split out of the
// constructors so a warp-specialized kernel can (a) reconfigure registers
// before any pipeline object exists and (b) keep the producer's and the
// consumer's pipeline objects in disjoint scopes — neither is ever live on
// the other's path, and idle warps never materialise one at all.
// All threads call this; only `initializing_warp` touches the mbarriers,
// but every thread must issue the init fence. Must be followed by a
// cluster-wide handshake before any barrier is used.
template <class ClusterShape>
CUTE_DEVICE static void init_barriers(
SharedStorage& storage,
int transaction_bytes,
int num_consumers,
ClusterShape cluster_shape) {
typename Type::Params params;
params.transaction_bytes = transaction_bytes;
params.num_producers = 1;
params.num_consumers = num_consumers;
Type::init_barriers(storage, params, cluster_shape);
}
template <class ClusterShape>
CUTE_DEVICE static Type make_producer(
SharedStorage& storage,
int transaction_bytes,
int num_consumers,
bool is_leader,
ClusterShape cluster_shape) {
typename Type::Params params;
params.transaction_bytes = transaction_bytes;
params.num_producers = 1;
params.num_consumers = num_consumers;
params.is_leader = is_leader;
params.role = Category::Producer;
// Barriers are already live; only the per-thread masks are built here.
return Type(storage, params, cluster_shape,
cute::false_type{}, cute::true_type{});
}
template <class ClusterShape>
CUTE_DEVICE static Type make_consumer(
SharedStorage& storage,
int transaction_bytes,
int num_consumers,
ClusterShape cluster_shape) {
typename Type::Params params;
params.transaction_bytes = transaction_bytes;
params.num_producers = 1;
params.num_consumers = num_consumers;
params.is_leader = 0;
params.role = Category::Consumer;
return Type(storage, params, cluster_shape,
cute::false_type{}, cute::true_type{});
}
CUTE_DEVICE static State producer_start_state() {
return cutlass::make_producer_start_state<Type>();
}
};
// ───────────────────────────────────────────────────────────────────────────
// Warp-group register reconfiguration and cluster synchronisation
// ───────────────────────────────────────────────────────────────────────────
template <int ProducerRegisters, int ConsumerRegisters, int Compute = 90>
struct WarpSpecializedRegistersSm90 {
static_assert(Compute == 90,
"WarpSpecializedRegistersSm90 requires Compute=90");
static_assert(ProducerRegisters % 8 == 0 && ConsumerRegisters % 8 == 0,
"setmaxnreg operands must be multiples of 8");
static constexpr int kCompute = Compute;
static constexpr int kProducerRegisters = ProducerRegisters;
static constexpr int kConsumerRegisters = ConsumerRegisters;
// Must be executed by every warp of the warp group, including idle ones.
CUTE_DEVICE static void producer() {
cutlass::arch::warpgroup_reg_dealloc<ProducerRegisters>();
}
CUTE_DEVICE static void consumer() {
cutlass::arch::warpgroup_reg_alloc<ConsumerRegisters>();
}
};
// pipeline_init_arrive / pipeline_init_wait: the cluster-wide handshake that
// must complete before any mbarrier in the cluster is touched.
template <int Compute = 90>
CUTE_DEVICE void cluster_pipeline_init_sm90() {
static_assert(Compute == 90,
"cluster_pipeline_init_sm90 requires Compute=90");
cute::cluster_arrive_relaxed();
cute::cluster_wait();
}
// No CTA may retire while a cluster peer can still address its SMEM.
template <int Compute = 90>
CUTE_DEVICE void cluster_exit_sm90() {
static_assert(Compute == 90, "cluster_exit_sm90 requires Compute=90");
cute::cluster_sync();
}
template <int BarrierId, int NumThreads, int Compute = 90>
CUTE_DEVICE void named_barrier_sync_sm90() {
static_assert(Compute == 90, "named_barrier_sync_sm90 requires Compute=90");
static_assert(BarrierId >= 0 && BarrierId < 16,
"hardware named barrier ids are 0..15");
static_assert(NumThreads % 32 == 0,
"named barrier thread counts must be warp aligned");
asm volatile("barrier.sync %0, %1;"
:
: "n"(BarrierId), "n"(NumThreads));
}
// ───────────────────────────────────────────────────────────────────────────
// Mainloop drivers
//
// These encode the CuTe-DSL pipeline *ordering* and nothing else. The caller
// supplies what to issue per stage; both functors are inlined, so the emitted
// instruction stream is identical to writing the loop out by hand.
// ───────────────────────────────────────────────────────────────────────────
template <int Compute = 90>
struct MainloopKLoopSm90 {
static_assert(Compute == 90, "MainloopKLoopSm90 requires Compute=90");
static constexpr int kCompute = Compute;
// Producer: acquire the stage, issue every TMA copy for it, advance.
// `issue(barrier, stage, k_tile)` performs the copies.
template <class Pipeline, class State, class IssueCopies>
CUTE_DEVICE static void produce(
Pipeline& pipe,
State& state,
int num_k_tiles,
IssueCopies issue) {
for (int k_tile = 0; k_tile < num_k_tiles; ++k_tile) {
pipe.producer_acquire(state);
issue(pipe.producer_get_barrier(state), state.index(), k_tile);
++state;
}
}
// Consumer with the deliberate one-stage release lag.
//
// Stage zero is issued without a release because no earlier WGMMA group
// has retired. Every later stage is committed, `wait_group(1)` retires the
// previous group, and only then is that previous AB stage handed back to
// the producer. The tail uses `wait_group(0)` before the final release.
// Without the lag, TMA would overwrite operands still feeding a live
// WGMMA group.
//
// `issue_mma(stage)` fences the accumulators, arrives, issues the GEMMs
// and commits the batch. `fence_accumulators()` re-fences them after a
// wait_group so the accumulator registers are readable again.
template <class Pipeline, class State, class IssueMma, class FenceAccumulators>
CUTE_DEVICE static void consume(
Pipeline& pipe,
State& read_state,
State& release_state,
int num_k_tiles,
IssueMma issue_mma,
FenceAccumulators fence_accumulators) {
pipe.consumer_wait(read_state);
issue_mma(read_state.index());
++read_state;
for (int k_tile = 1; k_tile < num_k_tiles; ++k_tile) {
pipe.consumer_wait(read_state);
issue_mma(read_state.index());
warpgroup_wait<1>();
fence_accumulators();
pipe.consumer_release(release_state);
++release_state;
++read_state;
}
warpgroup_wait<0>();
fence_accumulators();
pipe.consumer_release(release_state);
++release_state;
}
};
// ───────────────────────────────────────────────────────────────────────────
// Host-side cluster launch
// ───────────────────────────────────────────────────────────────────────────
template <int Compute = 90>
struct ClusterLaunchSm90 {
static_assert(Compute == 90, "ClusterLaunchSm90 requires Compute=90");
static constexpr int kCompute = Compute;
// Opt into the Hopper dynamic-SMEM ceiling and non-portable cluster sizes.
template <class Kernel>
static cudaError_t prepare(Kernel kernel, int smem_bytes) {
cudaError_t error = cudaFuncSetAttribute(
kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_bytes);
if (error != cudaSuccess) return error;
return cudaFuncSetAttribute(
kernel, cudaFuncAttributeNonPortableClusterSizeAllowed, 1);
}
static cudaLaunchConfig_t make_config(
dim3 grid,
int threads,
int smem_bytes,
int cluster_m,
cudaLaunchAttribute& attribute,
cudaStream_t stream) {
attribute = {};
attribute.id = cudaLaunchAttributeClusterDimension;
attribute.val.clusterDim.x = static_cast<unsigned>(cluster_m);
attribute.val.clusterDim.y = 1;
attribute.val.clusterDim.z = 1;
cudaLaunchConfig_t config = {};
config.gridDim = grid;
config.blockDim = dim3(static_cast<unsigned>(threads), 1, 1);
config.dynamicSmemBytes = smem_bytes;
config.stream = stream;
config.attrs = &attribute;
config.numAttrs = 1;
return config;
}
// Concurrent clusters this device can hold, i.e. the occupancy target a
// persistent/split scheduler should aim at. Returns 0 if unavailable.
template <class Kernel>
static int max_active_clusters(
Kernel kernel, int threads, int smem_bytes, int cluster_m) {
cudaLaunchAttribute attribute = {};
cudaLaunchConfig_t config = make_config(
dim3(static_cast<unsigned>(cluster_m), 1, 1),
threads,
smem_bytes,
cluster_m,
attribute,
nullptr);
int clusters = 0;
if (cudaOccupancyMaxActiveClusters(&clusters, kernel, &config) !=
cudaSuccess) {
cudaGetLastError();
return 0;
}
return clusters;
}
template <class Kernel, class... Args>
static cudaError_t launch(
Kernel kernel,
dim3 grid,
int threads,
int smem_bytes,
int cluster_m,
cudaStream_t stream,
Args const&... args) {
cudaLaunchAttribute attribute = {};
cudaLaunchConfig_t config = make_config(
grid, threads, smem_bytes, cluster_m, attribute, stream);
return cudaLaunchKernelEx(&config, kernel, args...);
}
};
} // namespace sm90
} // namespace liger
@@ -0,0 +1,118 @@
#pragma once
#include <cuda_runtime.h>
#include <cmath>
namespace liger {
namespace fused_scaled_linear_cross_entropy {
struct OnlineSoftmaxState {
float max_value;
float exp_sum;
float exp_weighted_sum;
float target_logit;
int has_target;
__host__ __device__ OnlineSoftmaxState()
: max_value(-1.0e38f),
exp_sum(0.0f),
exp_weighted_sum(0.0f),
target_logit(0.0f),
has_target(0) {}
template <bool ReturnEntropy>
__host__ __device__ void fold(float logit, bool valid, bool is_target) {
if (!valid) return;
float next_max = fmaxf(max_value, logit);
float previous_scale = exp_sum == 0.0f
? 0.0f
: expf(max_value - next_max);
float logit_scale = expf(logit - next_max);
exp_sum = exp_sum * previous_scale + logit_scale;
if constexpr (ReturnEntropy) {
exp_weighted_sum =
exp_weighted_sum * previous_scale + logit_scale * logit;
}
max_value = next_max;
if (is_target) {
target_logit = logit;
has_target = 1;
}
}
};
template <bool ReturnEntropy>
__host__ __device__ inline OnlineSoftmaxState merge_online_softmax(
const OnlineSoftmaxState& lhs, const OnlineSoftmaxState& rhs) {
if (lhs.exp_sum == 0.0f) return rhs;
if (rhs.exp_sum == 0.0f) return lhs;
OnlineSoftmaxState result;
result.max_value = fmaxf(lhs.max_value, rhs.max_value);
float lhs_scale = expf(lhs.max_value - result.max_value);
float rhs_scale = expf(rhs.max_value - result.max_value);
result.exp_sum = lhs.exp_sum * lhs_scale + rhs.exp_sum * rhs_scale;
if constexpr (ReturnEntropy) {
result.exp_weighted_sum =
lhs.exp_weighted_sum * lhs_scale + rhs.exp_weighted_sum * rhs_scale;
}
result.has_target = lhs.has_target || rhs.has_target;
result.target_logit = lhs.has_target ? lhs.target_logit : rhs.target_logit;
return result;
}
struct CorrectedSoftmaxStats {
float exp_sum;
float exp_weighted_sum;
float target_logit;
};
template <bool ReturnEntropy>
__host__ __device__ inline CorrectedSoftmaxStats correct_for_global_max(
const OnlineSoftmaxState& local, float global_max) {
if (local.exp_sum == 0.0f) {
return {0.0f, 0.0f, local.has_target ? local.target_logit : 0.0f};
}
float scale = expf(local.max_value - global_max);
float corrected_weighted_sum = 0.0f;
if constexpr (ReturnEntropy) {
corrected_weighted_sum = local.exp_weighted_sum * scale;
}
return {
local.exp_sum * scale,
corrected_weighted_sum,
local.has_target ? local.target_logit : 0.0f,
};
}
struct FinalizedSoftmax {
float lse;
float nll;
float entropy;
};
template <bool ReturnEntropy>
__host__ __device__ inline FinalizedSoftmax finalize_softmax(
float global_max,
float global_sum,
float global_target_logit,
float global_weighted_sum,
bool ignored) {
float lse = global_max + logf(global_sum);
float nll = ignored ? 0.0f : lse - global_target_logit;
float entropy = 0.0f;
if constexpr (ReturnEntropy) {
if (!ignored) {
entropy = lse - global_weighted_sum / global_sum;
}
}
return {lse, nll, entropy};
}
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,11 @@
#pragma once
namespace liger {
namespace fused_scaled_linear_cross_entropy {
// Invalidates cached FSLCE capacities, mappings, and duplicated teams before
// the shared BufferPool is cleared or NVSHMEM is finalized.
void reset_fslce_tp_configuration();
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,36 @@
#pragma once
// ═══════════════════════════════════════════════════════════════════════════
// SM90 fused scaled linear cross entropy — TMA store completion support.
//
// Transport selection and collective mappings live in liger_cute/detail.
// ═══════════════════════════════════════════════════════════════════════════
#include <cuda_runtime.h>
namespace liger {
namespace fused_scaled_linear_cross_entropy {
// ───────────────────────────────────────────────────────────────────────────
// TMA store completion helpers used by the dZ and dW epilogues.
// ───────────────────────────────────────────────────────────────────────────
// cp.async.bulk.wait_group N, always the full form and never `.read`.
//
// `.read` only waits for the SMEM source of a bulk store to be re-usable, and
// it *retires the group from the pending set*. A staging pipeline that drains
// with `.read` and then asks for `wait_group 0` therefore gets no guarantee at
// all about the already-retired groups' global writes. Every tile here is
// published to another warp the moment it is stored, so the epilogue has to
// wait for completion, not merely for the source to be free.
template <int Pending>
__device__ __forceinline__ void wait_tma_store() {
asm volatile("cp.async.bulk.wait_group %0;" :: "n"(Pending) : "memory");
}
__device__ __forceinline__ void wait_tma_store_complete() {
wait_tma_store<0>();
}
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,429 @@
#include "workspace.cuh"
#include <cuda_runtime.h>
#include <cstddef>
#include <cstdint>
#include <limits>
#include "buffer_pool.cuh"
#include "forward_reduce.cuh"
#include "liger_cute/check.h"
#include "liger_cute/detail/tp_reduce.cuh"
namespace liger {
namespace fused_scaled_linear_cross_entropy {
namespace {
using Config = BackwardGemmConfigSm90<90>;
using Launch = BackwardGemmLaunchSm90<90>;
// The ring depth is a compile-time constant shared with the kernel template
// (dx_reduce.cuh): the symmetric capacity and CTA-local mbarrier indexing have
// to match what the kernel uses.
constexpr int kConfiguredStages = kDxRingStages;
BackwardTpCapacity g_capacity = {};
bool g_configured = false;
std::size_t g_staging_bytes = 0;
std::size_t g_durable_bytes = 0;
std::size_t g_packed_durable_bytes = 0;
std::size_t g_reduced_bytes = 0;
std::size_t checked_multiply(std::size_t lhs, std::size_t rhs, const char* name) {
LIGER_CHECK(
rhs == 0 || lhs <= std::numeric_limits<std::size_t>::max() / rhs,
name,
" size overflow");
return lhs * rhs;
}
std::size_t staging_bytes_at(int tiles_per_reduce, int num_ctas) {
std::size_t tile_elements = checked_multiply(
static_cast<std::size_t>(Config::kTileM),
static_cast<std::size_t>(Config::kDxTileN),
"dX tile");
std::size_t elements = checked_multiply(
tile_elements,
static_cast<std::size_t>(tiles_per_reduce),
"dX group");
elements = checked_multiply(
elements, static_cast<std::size_t>(kConfiguredStages), "dX ring");
elements = checked_multiply(
elements,
static_cast<std::size_t>(kDxCommWarpsPerChannel),
"dX comm warps");
elements = checked_multiply(
elements, static_cast<std::size_t>(num_ctas), "dX resident CTAs");
return checked_multiply(elements, sizeof(float), "dX staging");
}
std::size_t sync_bytes_at(int num_ctas, int team_size) {
std::size_t entries = checked_multiply(
static_cast<std::size_t>(num_ctas),
static_cast<std::size_t>(kDxCommWarpsPerChannel),
"dX sync CTAs");
entries = checked_multiply(
entries, static_cast<std::size_t>(kConfiguredStages), "dX sync ring");
entries = checked_multiply(
entries, static_cast<std::size_t>(kDxSyncPhases), "dX sync phases");
entries = checked_multiply(
entries, static_cast<std::size_t>(team_size), "dX sync team");
return checked_multiply(entries, sizeof(std::uint64_t), "dX sync");
}
std::size_t durable_bytes_at(int tokens, int hidden) {
LIGER_CHECK(tokens > 0, "max_tokens must be positive");
LIGER_CHECK(hidden > 0, "max_hidden must be positive");
std::size_t waves =
(static_cast<std::size_t>(tokens) + Config::kWaveRows - 1) /
Config::kWaveRows;
std::size_t rows = checked_multiply(
waves, static_cast<std::size_t>(Config::kWaveRows), "dX durable rows");
std::size_t n_tiles =
(static_cast<std::size_t>(hidden) + Config::kDxTileN - 1) /
Config::kDxTileN;
std::size_t columns = checked_multiply(
n_tiles,
static_cast<std::size_t>(Config::kDxTileN),
"dX durable columns");
return checked_multiply(
checked_multiply(rows, columns, "dX durable elements"),
sizeof(float),
"dX durable");
}
void ensure_configured() {
LIGER_CHECK(
g_configured,
"fused_scaled_linear_cross_entropy backward: call "
"configure_backward_tp_symmetric(max_tokens, max_hidden, "
"max_local_vocab, "
"max_tiles_per_reduce, max_comm_channels, team_handle) collectively on "
"every PE before the first launch");
}
void check_cuda(cudaError_t error, const char* what) {
LIGER_CHECK(
error == cudaSuccess,
"fused_scaled_linear_cross_entropy backward: ",
what,
" failed: ",
cudaGetErrorString(error));
}
int resident_cta_capacity() {
int device = 0;
check_cuda(cudaGetDevice(&device), "cudaGetDevice");
cudaDeviceProp properties = {};
check_cuda(
cudaGetDeviceProperties(&properties, device),
"cudaGetDeviceProperties");
LIGER_CHECK(
properties.multiProcessorCount > 0 &&
properties.multiProcessorCount <= kMaxDxResidentCtas,
"fused_scaled_linear_cross_entropy backward: SM90 device reports ",
properties.multiProcessorCount,
" SMs, but the CTA-owned dX workspace supports at most ",
kMaxDxResidentCtas);
return properties.multiProcessorCount;
}
} // namespace
void configure_backward_tp_symmetric(
int max_tokens,
int max_hidden,
int max_local_vocab,
int max_tiles_per_reduce,
int max_comm_channels,
std::int64_t team_handle) {
LIGER_CHECK(max_tokens > 0, "max_tokens must be positive");
LIGER_CHECK(max_hidden > 0, "max_hidden must be positive");
LIGER_CHECK(max_local_vocab > 0, "max_local_vocab must be positive");
LIGER_CHECK(
max_tiles_per_reduce >= 1,
"max_tiles_per_reduce must be positive");
LIGER_CHECK(
max_comm_channels >= 1,
"max_comm_channels must be positive");
int max_resident_ctas = resident_cta_capacity();
liger_cute::detail::TpReduceTopology topology =
liger_cute::detail::query_tp_reduce_topology(team_handle);
int team_size = topology.team_size;
if (g_configured) {
LIGER_CHECK(
max_tokens <= g_capacity.max_tokens &&
max_hidden <= g_capacity.max_hidden &&
max_local_vocab <= g_capacity.max_local_vocab &&
max_tiles_per_reduce <= g_capacity.max_tiles_per_reduce &&
max_comm_channels <= g_capacity.max_comm_channels &&
max_resident_ctas == g_capacity.max_resident_ctas &&
team_size == g_capacity.team_size &&
team_handle == g_capacity.team_handle,
"fused_scaled_linear_cross_entropy backward: the symmetric "
"capacity is immutable once allocated (configured for vocab ",
g_capacity.max_local_vocab,
", tokens ",
g_capacity.max_tokens,
", hidden ",
g_capacity.max_hidden,
", TilesPerReduce ",
g_capacity.max_tiles_per_reduce,
", communication channels ",
g_capacity.max_comm_channels,
", resident CTAs ",
g_capacity.max_resident_ctas,
"). Configure the maximum upfront.");
return;
}
using Names = BackwardSymmetricNames;
auto& pool = global_buffer_pool();
// One fixed order on every PE: symmetric first, then device private.
g_staging_bytes = staging_bytes_at(
max_tiles_per_reduce, max_resident_ctas);
g_durable_bytes = durable_bytes_at(max_tokens, max_hidden);
int local_partition_size = topology.local_size;
g_packed_durable_bytes =
(g_durable_bytes + static_cast<std::size_t>(local_partition_size) - 1) /
static_cast<std::size_t>(local_partition_size);
g_reduced_bytes = local_partition_size > 1
? (g_staging_bytes > g_durable_bytes
? g_staging_bytes
: g_durable_bytes)
: g_staging_bytes;
auto* partial = static_cast<float*>(
pool.get_symmetric(Names::kDxPartial, g_staging_bytes));
auto* reduced = static_cast<float*>(
pool.get_symmetric(Names::kDxReduced, g_reduced_bytes));
auto* reduced_shard = static_cast<float*>(
pool.get_symmetric(
Names::kDxReducedShard, g_packed_durable_bytes));
auto* remote_inbox = static_cast<float*>(
pool.get_symmetric(
Names::kDxRemoteInbox, g_packed_durable_bytes));
auto* remote_signals = static_cast<std::uint64_t*>(
pool.get_symmetric(
Names::kDxRemoteSignals,
2 * sizeof(std::uint64_t)));
std::size_t sync_bytes = sync_bytes_at(max_resident_ctas, team_size);
auto* sync = static_cast<std::uint64_t*>(
pool.get_symmetric(Names::kDxSync, sync_bytes));
auto** peer_partial_storage = static_cast<float**>(
pool.get_device(
Names::kDxPeerPartialPointers,
static_cast<std::size_t>(team_size) * sizeof(float*)));
auto** peer_sync_storage = static_cast<std::uint64_t**>(
pool.get_device(
Names::kDxPeerSyncPointers,
static_cast<std::size_t>(team_size) *
sizeof(std::uint64_t*)));
pool.get_device(
Names::kDzWorkspace, Launch::dz_workspace_bytes(max_local_vocab));
auto* launch_epoch = static_cast<std::uint64_t*>(
pool.get_device(Names::kDxLaunchEpoch, sizeof(std::uint64_t)));
check_cuda(
cudaMemset(launch_epoch, 0, sizeof(std::uint64_t)),
"cudaMemset(dX launch epoch)");
liger_cute::detail::configure_tp_reduce(
team_handle,
{
partial,
reduced,
reduced_shard,
remote_inbox,
remote_signals,
sync,
sync_bytes,
peer_partial_storage,
peer_sync_storage,
});
g_capacity.max_tokens = max_tokens;
g_capacity.max_hidden = max_hidden;
g_capacity.max_local_vocab = max_local_vocab;
g_capacity.max_tiles_per_reduce = max_tiles_per_reduce;
g_capacity.max_comm_channels = max_comm_channels;
g_capacity.max_resident_ctas = max_resident_ctas;
g_capacity.max_stages = kConfiguredStages;
g_capacity.team_size = team_size;
g_capacity.team_handle = team_handle;
g_configured = true;
}
std::size_t backward_tp_pool_symmetric_bytes(
int max_tokens,
int max_hidden,
int max_tiles_per_reduce,
int max_comm_channels) {
LIGER_CHECK(
max_comm_channels >= 1,
"max_comm_channels must be positive");
int max_ctas =
g_configured ? g_capacity.max_resident_ctas : resident_cta_capacity();
int team_size = g_configured
? g_capacity.team_size
: liger_cute::detail::kMaxTpReduceTeamSize;
if (g_configured) {
return g_staging_bytes + g_reduced_bytes +
2u * g_packed_durable_bytes +
sync_bytes_at(max_ctas, team_size) +
2 * sizeof(std::uint64_t);
}
std::size_t staging = staging_bytes_at(max_tiles_per_reduce, max_ctas);
std::size_t durable = durable_bytes_at(max_tokens, max_hidden);
std::size_t reduced = staging > durable ? staging : durable;
return staging + reduced + 2u * durable +
sync_bytes_at(max_ctas, team_size) +
2 * sizeof(std::uint64_t);
}
std::size_t backward_tp_pool_device_bytes(int max_local_vocab) {
int team_size = g_configured
? g_capacity.team_size
: liger_cute::detail::kMaxTpReduceTeamSize;
std::size_t peer_mapping =
static_cast<std::size_t>(team_size) *
(2 * sizeof(void*));
return Launch::dz_workspace_bytes(max_local_vocab) +
sizeof(std::uint64_t) + peer_mapping;
}
DxReduceWorkspace<float> reserve_dx_reduce_workspace(
int tiles_per_reduce,
int num_stages,
int num_ctas) {
ensure_configured();
LIGER_CHECK(
tiles_per_reduce >= 1 &&
tiles_per_reduce <= g_capacity.max_tiles_per_reduce,
"fused_scaled_linear_cross_entropy backward: TilesPerReduce ",
tiles_per_reduce,
" exceeds the configured maximum ",
g_capacity.max_tiles_per_reduce);
LIGER_CHECK(
num_stages == g_capacity.max_stages,
"fused_scaled_linear_cross_entropy backward: the staging ring depth ",
num_stages,
" must equal the configured ",
g_capacity.max_stages,
"; CTA-local mbarriers use the compile-time depth");
LIGER_CHECK(
num_ctas >= 1 && num_ctas <= g_capacity.max_resident_ctas,
"fused_scaled_linear_cross_entropy backward: launch grid ",
num_ctas,
" exceeds resident CTA capacity ",
g_capacity.max_resident_ctas);
using Names = BackwardSymmetricNames;
auto& pool = global_buffer_pool();
// Always the configured capacity, never the per-call size.
std::size_t sync_bytes = sync_bytes_at(
g_capacity.max_resident_ctas, g_capacity.team_size);
DxReduceWorkspace<float> workspace = {};
workspace.partial = static_cast<float*>(
pool.get_symmetric(Names::kDxPartial, g_staging_bytes));
workspace.reduced = static_cast<float*>(
pool.get_symmetric(Names::kDxReduced, g_reduced_bytes));
workspace.sync = static_cast<std::uint64_t*>(
pool.get_symmetric(Names::kDxSync, sync_bytes));
workspace.launch_epoch = static_cast<const std::uint64_t*>(
pool.get_device(Names::kDxLaunchEpoch, sizeof(std::uint64_t)));
return workspace;
}
std::size_t backward_dx_staging_bytes(int max_tiles_per_reduce) {
int max_ctas =
g_configured ? g_capacity.max_resident_ctas : resident_cta_capacity();
return staging_bytes_at(max_tiles_per_reduce, max_ctas);
}
std::size_t backward_dx_configured_staging_bytes() {
ensure_configured();
return staging_bytes_at(
g_capacity.max_tiles_per_reduce, g_capacity.max_resident_ctas);
}
std::size_t backward_dx_configured_durable_bytes() {
ensure_configured();
return g_durable_bytes;
}
void validate_backward_tp_shape(
int tokens, int hidden, int local_vocab) {
ensure_configured();
LIGER_CHECK(
tokens > 0 && tokens <= g_capacity.max_tokens,
"fused_scaled_linear_cross_entropy backward: tokens ",
tokens,
" exceed the configured maximum ",
g_capacity.max_tokens);
LIGER_CHECK(
hidden > 0 && hidden <= g_capacity.max_hidden,
"fused_scaled_linear_cross_entropy backward: hidden ",
hidden,
" exceeds the configured maximum ",
g_capacity.max_hidden);
LIGER_CHECK(
local_vocab > 0 && local_vocab <= g_capacity.max_local_vocab,
"fused_scaled_linear_cross_entropy backward: local_vocab ",
local_vocab,
" exceeds the configured maximum ",
g_capacity.max_local_vocab);
LIGER_CHECK(
durable_bytes_at(tokens, hidden) <=
backward_dx_configured_durable_bytes(),
"fused_scaled_linear_cross_entropy backward: cluster dX durable "
"workspace capacity exceeded");
}
int backward_dx_resident_cta_capacity() {
return g_configured ? g_capacity.max_resident_ctas : resident_cta_capacity();
}
int backward_dx_team_size() {
ensure_configured();
return g_capacity.team_size;
}
std::int64_t backward_dx_team_handle() {
ensure_configured();
return g_capacity.team_handle;
}
void reset_fslce_tp_configuration() {
liger_cute::detail::reset_tp_reduce();
g_capacity = {};
g_configured = false;
g_staging_bytes = 0;
g_durable_bytes = 0;
g_packed_durable_bytes = 0;
g_reduced_bytes = 0;
reset_forward_tp_workspace_configuration();
}
BackwardScratch reserve_backward_scratch(int local_vocab) {
ensure_configured();
LIGER_CHECK(
local_vocab > 0 && local_vocab <= g_capacity.max_local_vocab,
"fused_scaled_linear_cross_entropy backward: local_vocab ",
local_vocab,
" exceeds the configured maximum ",
g_capacity.max_local_vocab);
using Names = BackwardSymmetricNames;
auto& pool = global_buffer_pool();
std::size_t dz_bytes =
Launch::dz_workspace_bytes(g_capacity.max_local_vocab);
BackwardScratch scratch = {};
scratch.dz_workspace = pool.get_device(Names::kDzWorkspace, dz_bytes);
scratch.dz_workspace_bytes = dz_bytes;
return scratch;
}
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -0,0 +1,147 @@
#pragma once
// ═══════════════════════════════════════════════════════════════════════════
// Backward workspace ownership.
//
// ── Allocator ownership ───────────────────────────────────────────────────
// Nothing here allocates memory itself: there is no local allocator and no raw
// nvshmem_malloc / nvshmem_free lifecycle anywhere in the backward path. Every
// buffer comes from liger::global_buffer_pool(), which is the shared MoE
// utility for fixed, reusable, internal staging that never escapes the
// operation.
//
// name allocator symmetric? why
// ----------------- --------- ---------- ---------------------------
// kDxPartial pool yes FP32 NVLS SUM source
// kDxReduced pool yes FP32 NVLS SUM destination
// kDxSync pool yes per-slot ready/completion epochs
// kDzWorkspace pool no one wave of dZ, BF16
//
// liger::global_symmetric_stack() is deliberately NOT used. Nothing produced by
// the forward has to be carried to the backward as symmetric state: the
// forward's two all-reduces leave every rank holding identical NLL / LSE /
// entropy, so those are ordinary local CUDA outputs saved through autograd.
// Backward consumes them exactly like any other saved tensor.
//
// ── Collective configuration ──────────────────────────────────────────────
// configure_backward_tp_symmetric() must run on every PE with the same values
// before the first launch. It fixes the capacity of every allocation named
// here and resolves their aliases in the TP team's multicast mapping.
// Capacities are immutable afterwards: every later request asks for the
// *configured* capacity, never the per-call size, so the byte count handed to
// the pool is identical on every call and on every PE.
// ═══════════════════════════════════════════════════════════════════════════
#include <cuda_bf16.h>
#include <cuda_runtime.h>
#include <cstddef>
#include <cstdint>
#include "backward_gemm_sm90.cuh"
#include "dx_reduce.cuh"
#include "state.h"
namespace liger {
namespace fused_scaled_linear_cross_entropy {
// Centralized allocation names. Nothing else in the backward path may name a
// pooled buffer.
struct BackwardSymmetricNames {
// global_buffer_pool() — symmetric (remotely addressed by NVSHMEM).
static constexpr const char* kDxPartial =
"fused_scaled_linear_cross_entropy_tp_dx_partial";
static constexpr const char* kDxReduced =
"fused_scaled_linear_cross_entropy_tp_dx_reduced";
static constexpr const char* kDxReducedShard =
"fused_scaled_linear_cross_entropy_tp_dx_reduced_shard";
static constexpr const char* kDxRemoteInbox =
"fused_scaled_linear_cross_entropy_tp_dx_remote_inbox";
static constexpr const char* kDxRemoteSignals =
"fused_scaled_linear_cross_entropy_tp_dx_remote_signals";
static constexpr const char* kDxSync =
"fused_scaled_linear_cross_entropy_tp_dx_sync";
// global_buffer_pool() — device private (never remotely addressed).
static constexpr const char* kDzWorkspace =
"fused_scaled_linear_cross_entropy_tp_dz_workspace";
static constexpr const char* kDxPeerPartialPointers =
"fused_scaled_linear_cross_entropy_tp_dx_peer_partial_ptrs";
static constexpr const char* kDxPeerSyncPointers =
"fused_scaled_linear_cross_entropy_tp_dx_peer_sync_ptrs";
static constexpr const char* kDxLaunchEpoch =
"fused_scaled_linear_cross_entropy_tp_dx_launch_epoch";
};
// The immutable collective configuration.
struct BackwardTpCapacity {
int max_tokens;
int max_hidden;
int max_local_vocab;
int max_tiles_per_reduce;
int max_comm_channels;
int max_resident_ctas;
int max_stages;
int team_size;
std::int64_t team_handle;
};
// Collective. Must be called on every PE with the same values before the first
// launch. CTA-owned NVLS staging is sized for full residency;
// `max_comm_channels` is retained for API compatibility; the CTA-owned
// production path does not allocate per-channel teams or buffers.
void configure_backward_tp_symmetric(
int max_tokens,
int max_hidden,
int max_local_vocab,
int max_tiles_per_reduce,
int max_comm_channels,
std::int64_t team_handle);
// Symmetric footprint at the configured maximum. The fused path reuses the
// larger CTA-owned staging allocation, so channels add no buffer bytes.
std::size_t backward_tp_pool_symmetric_bytes(
int max_tokens,
int max_hidden,
int max_tiles_per_reduce,
int max_comm_channels);
// Total device-private pool footprint. CTA ring state is in shared memory.
std::size_t backward_tp_pool_device_bytes(int max_local_vocab);
// Collective: every PE of the configured team must call this with the same
// arguments. Returns local and multicast staging pointers plus TP metadata.
// Every pool request asks for the *configured* capacity,
// so the byte count is identical on every PE and on every call; only the
// descriptive fields of the returned workspace follow the launch grid.
//
// The knobs are runtime here on purpose: the compile-time TilesPerReduce /
// NumStages of the kernel template are validated against them in the launcher,
// which keeps a single allocation path for every instantiation.
DxReduceWorkspace<float> reserve_dx_reduce_workspace(
int tiles_per_reduce, int num_stages, int num_ctas);
// Bytes in one of the two FP32 staging allocations at configured capacity.
std::size_t backward_dx_staging_bytes(int max_tiles_per_reduce);
std::size_t backward_dx_configured_staging_bytes();
std::size_t backward_dx_configured_durable_bytes();
void validate_backward_tp_shape(
int tokens, int hidden, int local_vocab);
// Full resident CTA capacity used by symmetric staging on the current device.
int backward_dx_resident_cta_capacity();
int backward_dx_team_size();
std::int64_t backward_dx_team_handle();
// The pooled dZ wave workspace for this shape.
struct BackwardScratch {
void* dz_workspace;
std::size_t dz_workspace_bytes;
};
BackwardScratch reserve_backward_scratch(int local_vocab);
} // namespace fused_scaled_linear_cross_entropy
} // namespace liger
@@ -24,6 +24,7 @@
#include "liger_cute/detail/comm_schedule.cuh"
#include "liger_cute/detail/status.h"
#include "liger_cute/detail/symmetric_memory.h"
#include "fused_scaled_linear_cross_entropy/state.h"
namespace liger_cute {
namespace detail {
@@ -77,6 +78,7 @@ void init_comm_schedule(int N, int M) {
// deterministic across PEs so the collective nvshmem_free calls stay in
// lockstep.
void clear_global_pools() {
liger::fused_scaled_linear_cross_entropy::reset_fslce_tp_configuration();
global_symmetric_stack().clear();
global_buffer_pool().clear();
}
@@ -267,6 +269,7 @@ liger_cute_status_t liger_cute_pool_clear_all(void) {
liger_cute_status_t liger_cute_pool_clear_buffers(void) {
return liger_cute::detail::guarded([&]() -> liger_cute_status_t {
liger::fused_scaled_linear_cross_entropy::reset_fslce_tp_configuration();
liger_cute::detail::global_buffer_pool().clear();
return LIGER_CUTE_OK;
});
@@ -78,6 +78,28 @@ def init_pmi() -> None:
_load_module().init_pmi()
def nccl_unique_id_nbytes() -> int:
out = _int64_out()
_load_module().nccl_unique_id_nbytes(out)
return int(out.item())
def nccl_get_unique_id() -> torch.Tensor:
out = torch.empty(nccl_unique_id_nbytes(), dtype=torch.uint8, device="cpu")
_load_module().nccl_get_unique_id(out)
return out
def nccl_comm_init_rank(rank: int, nranks: int, unique_id: torch.Tensor) -> int:
out = _int64_out()
_load_module().nccl_comm_init_rank(int(rank), int(nranks), unique_id, out)
return int(out.item())
def nccl_comm_destroy(comm_handle: int) -> None:
_load_module().nccl_comm_destroy(int(comm_handle))
def finalize() -> None:
_load_module().finalize()
@@ -249,3 +271,101 @@ def moe_fused_bwd_bf16(
dW,
)
return dX, dB, dC, dA, dW
def fused_linear_scaled_cross_entropy_configure_forward(
max_tokens: int,
max_local_vocab: int,
) -> None:
_load_module().fused_linear_scaled_cross_entropy_configure_forward(
int(max_tokens),
int(max_local_vocab),
)
def fused_linear_scaled_cross_entropy_configure_backward(
max_tokens: int,
max_hidden: int,
max_local_vocab: int,
max_tiles_per_reduce: int,
team_handle: int,
) -> None:
_load_module().fused_linear_scaled_cross_entropy_configure_backward(
int(max_tokens),
int(max_hidden),
int(max_local_vocab),
int(max_tiles_per_reduce),
int(team_handle),
)
def fused_linear_scaled_cross_entropy_forward(
x: torch.Tensor,
weight: torch.Tensor,
target: torch.Tensor,
vocab_start: int,
ignore_index: int,
inverse_temperature: float,
nccl_comm_handle: int,
return_entropy: bool,
):
"""Run the complete tensor-parallel forward using a caller-owned NCCL communicator."""
tokens = x.shape[0]
nll = torch.empty(tokens, dtype=torch.float32, device=x.device)
lse = torch.empty(tokens, dtype=torch.float32, device=x.device)
entropy = (
torch.empty(tokens, dtype=torch.float32, device=x.device)
if return_entropy
else torch.zeros(tokens, dtype=torch.float32, device=x.device)
)
_load_module().fused_linear_scaled_cross_entropy_forward(
x,
weight,
target,
int(vocab_start),
int(ignore_index),
float(inverse_temperature),
int(nccl_comm_handle),
bool(return_entropy),
nll,
lse,
entropy,
)
return nll, lse, entropy
def fused_linear_scaled_cross_entropy_backward(
grad_output: torch.Tensor,
entropy_grad: torch.Tensor,
x: torch.Tensor,
weight: torch.Tensor,
target: torch.Tensor,
lse: torch.Tensor,
entropy: torch.Tensor,
vocab_start: int,
ignore_index: int,
inverse_temperature: float,
team_handle: int,
tiles_per_reduce: int,
return_entropy: bool,
):
grad_input = torch.empty_like(x)
grad_weight = torch.empty_like(weight)
_load_module().fused_linear_scaled_cross_entropy_backward(
grad_output,
entropy_grad,
x,
weight,
target,
lse,
entropy,
int(vocab_start),
int(ignore_index),
float(inverse_temperature),
int(team_handle),
int(tiles_per_reduce),
bool(return_entropy),
grad_input,
grad_weight,
)
return grad_input, grad_weight
@@ -2,11 +2,18 @@
#include <tvm/ffi/tvm_ffi.h>
#include <cuda_runtime.h>
#include <nccl.h>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <exception>
#include "backward_gemm_sm90.cuh"
#include "forward_gemm_sm90.cuh"
#include "forward_reduce.cuh"
#include "workspace.cuh"
#include "moe_launch.h"
namespace liger {
@@ -68,6 +75,13 @@ void CheckStatus(liger_cute_status_t status, const char* what) {
}
}
void CheckNccl(ncclResult_t status, const char* what) {
if (status != ncclSuccess) {
TVM_FFI_THROW(RuntimeError) << "liger_cute: " << what << " failed: "
<< ncclGetErrorString(status);
}
}
void RequireTensor(ffi::TensorView tensor, int ndim, DLDataType dtype, const char* name) {
TVM_FFI_ICHECK_EQ(tensor.ndim(), ndim) << name;
TVM_FFI_ICHECK_EQ(tensor.dtype(), dtype) << name;
@@ -79,6 +93,18 @@ void RequireCudaTensor(ffi::TensorView tensor, int ndim, DLDataType dtype, const
TVM_FFI_ICHECK_EQ(tensor.device().device_type, kDLCUDA) << name;
}
void RequireSameCudaDevice(ffi::TensorView tensor, ffi::TensorView reference, const char* name) {
TVM_FFI_ICHECK_EQ(tensor.device().device_type, reference.device().device_type)
<< name << " must be on the same device as x";
TVM_FFI_ICHECK_EQ(tensor.device().device_id, reference.device().device_id)
<< name << " must be on the same device as x";
}
void RequirePositiveInverseTemperature(double inverse_temperature) {
TVM_FFI_ICHECK(std::isfinite(inverse_temperature) && inverse_temperature > 0.0)
<< "inverse_temperature must be finite and positive";
}
void RequireCudaMoeWeight(ffi::TensorView tensor, DLDataType dtype, const char* name) {
TVM_FFI_ICHECK_EQ(tensor.ndim(), 3) << name;
TVM_FFI_ICHECK_EQ(tensor.dtype(), dtype) << name;
@@ -160,6 +186,52 @@ void finalize() { CheckStatus(liger_cute_nvshmem_finalize(), "finalize"); }
void pool_clear_all() { CheckStatus(liger_cute_pool_clear_all(), "pool_clear_all"); }
void pool_clear_buffers() { CheckStatus(liger_cute_pool_clear_buffers(), "pool_clear_buffers"); }
void nccl_unique_id_nbytes(ffi::TensorView out) {
RequireCpuInt64(out, 1);
static_cast<int64_t*>(out.data_ptr())[0] = sizeof(ncclUniqueId);
}
void nccl_get_unique_id(ffi::TensorView out) {
DLDataType u8{kDLUInt, 8, 1};
RequireTensor(out, 1, u8, "out");
TVM_FFI_ICHECK_EQ(out.device().device_type, kDLCPU);
TVM_FFI_ICHECK_EQ(out.size(0), static_cast<int64_t>(sizeof(ncclUniqueId)));
ncclUniqueId unique_id;
CheckNccl(ncclGetUniqueId(&unique_id), "ncclGetUniqueId");
std::memcpy(out.data_ptr(), &unique_id, sizeof(unique_id));
}
void nccl_comm_init_rank(
int64_t rank, int64_t nranks, ffi::TensorView unique_id, ffi::TensorView out) {
DLDataType u8{kDLUInt, 8, 1};
RequireTensor(unique_id, 1, u8, "unique_id");
TVM_FFI_ICHECK_EQ(unique_id.device().device_type, kDLCPU);
TVM_FFI_ICHECK_EQ(
unique_id.size(0), static_cast<int64_t>(sizeof(ncclUniqueId)));
RequireCpuInt64(out, 1);
TVM_FFI_ICHECK_GT(nranks, 0);
TVM_FFI_ICHECK_GE(rank, 0);
TVM_FFI_ICHECK_LT(rank, nranks);
ncclUniqueId id;
std::memcpy(&id, unique_id.data_ptr(), sizeof(id));
ncclComm_t comm = nullptr;
CheckNccl(
ncclCommInitRank(
&comm, static_cast<int>(nranks), id, static_cast<int>(rank)),
"ncclCommInitRank");
static_cast<int64_t*>(out.data_ptr())[0] =
reinterpret_cast<int64_t>(comm);
}
void nccl_comm_destroy(int64_t comm_handle) {
if (comm_handle == 0) {
return;
}
CheckNccl(
ncclCommDestroy(reinterpret_cast<ncclComm_t>(comm_handle)),
"ncclCommDestroy");
}
void my_pe(ffi::TensorView out) {
RequireCpuInt32(out, 1);
CheckStatus(liger_cute_nvshmem_my_pe(static_cast<int*>(out.data_ptr())), "my_pe");
@@ -384,6 +456,181 @@ void moe_fused_bwd_bf16(
}
}
void fused_linear_scaled_cross_entropy_configure_forward(
int64_t max_tokens, int64_t max_local_vocab) {
TVM_FFI_ICHECK_GT(max_tokens, 0);
TVM_FFI_ICHECK_GT(max_local_vocab, 0);
try {
liger::fused_scaled_linear_cross_entropy::configure_forward_tp_workspace(
static_cast<int>(max_tokens), static_cast<int>(max_local_vocab));
} catch (const std::exception& e) {
ThrowCoreError("fused_linear_scaled_cross_entropy_configure_forward", e);
}
}
void fused_linear_scaled_cross_entropy_configure_backward(
int64_t max_tokens, int64_t max_hidden, int64_t max_local_vocab,
int64_t max_tiles_per_reduce, int64_t team_handle) {
TVM_FFI_ICHECK_GT(max_tokens, 0);
TVM_FFI_ICHECK_GT(max_hidden, 0);
TVM_FFI_ICHECK_GT(max_local_vocab, 0);
TVM_FFI_ICHECK(
max_tiles_per_reduce == 1 || max_tiles_per_reduce == 2 ||
max_tiles_per_reduce == 4);
try {
liger::fused_scaled_linear_cross_entropy::configure_backward_tp_symmetric(
static_cast<int>(max_tokens), static_cast<int>(max_hidden),
static_cast<int>(max_local_vocab), static_cast<int>(max_tiles_per_reduce), 1, team_handle);
} catch (const std::exception& e) {
ThrowCoreError("fused_linear_scaled_cross_entropy_configure_backward", e);
}
}
void fused_linear_scaled_cross_entropy_forward(
ffi::TensorView x, ffi::TensorView weight, ffi::TensorView target,
int64_t vocab_start, int64_t ignore_index, double inverse_temperature,
int64_t nccl_comm_handle, bool return_entropy, ffi::TensorView nll,
ffi::TensorView lse, ffi::TensorView entropy) {
DLDataType bf16{kDLBfloat, 16, 1};
DLDataType f32{kDLFloat, 32, 1};
DLDataType i64{kDLInt, 64, 1};
RequireCudaTensor(x, 2, bf16, "x");
RequireCudaTensor(weight, 2, bf16, "weight");
RequireCudaTensor(target, 1, i64, "target");
RequireCudaTensor(nll, 1, f32, "nll");
RequireCudaTensor(lse, 1, f32, "lse");
RequireCudaTensor(entropy, 1, f32, "entropy");
RequireSameCudaDevice(weight, x, "weight");
RequireSameCudaDevice(target, x, "target");
RequireSameCudaDevice(nll, x, "nll");
RequireSameCudaDevice(lse, x, "lse");
RequireSameCudaDevice(entropy, x, "entropy");
int64_t tokens = x.size(0);
int64_t hidden = x.size(1);
int64_t local_vocab = weight.size(0);
TVM_FFI_ICHECK_EQ(weight.size(1), hidden);
TVM_FFI_ICHECK_EQ(target.size(0), tokens);
TVM_FFI_ICHECK_EQ(nll.size(0), tokens);
TVM_FFI_ICHECK_EQ(lse.size(0), tokens);
TVM_FFI_ICHECK_EQ(entropy.size(0), tokens);
TVM_FFI_ICHECK_GE(vocab_start, 0);
TVM_FFI_ICHECK_NE(nccl_comm_handle, 0);
RequirePositiveInverseTemperature(inverse_temperature);
using namespace liger::fused_scaled_linear_cross_entropy;
ForwardTpParamsSm90<90> params;
params.gemm.x = x.data_ptr();
params.gemm.weight = weight.data_ptr();
params.gemm.target = static_cast<const int64_t*>(target.data_ptr());
params.gemm.tokens = static_cast<int>(tokens);
params.gemm.hidden = static_cast<int>(hidden);
params.gemm.local_vocab = static_cast<int>(local_vocab);
params.gemm.vocab_start = vocab_start;
params.gemm.ignore_index = ignore_index;
params.gemm.inverse_temperature = static_cast<float>(inverse_temperature);
params.nll = static_cast<float*>(nll.data_ptr());
params.lse = static_cast<float*>(lse.data_ptr());
params.entropy = static_cast<float*>(entropy.data_ptr());
params.nccl_comm_handle = nccl_comm_handle;
cudaStream_t stream = reinterpret_cast<cudaStream_t>(
TVMFFIEnvGetStream(x.device().device_type, x.device().device_id));
try {
if (return_entropy) {
liger::fused_scaled_linear_cross_entropy::
fused_linear_scaled_cross_entropy_forward<true, 90>(params, stream);
} else {
liger::fused_scaled_linear_cross_entropy::
fused_linear_scaled_cross_entropy_forward<false, 90>(params, stream);
}
} catch (const std::exception& e) {
ThrowCoreError("fused_linear_scaled_cross_entropy_forward", e);
}
}
void fused_linear_scaled_cross_entropy_backward(
ffi::TensorView grad_output, ffi::TensorView entropy_grad, ffi::TensorView x,
ffi::TensorView weight, ffi::TensorView target, ffi::TensorView lse,
ffi::TensorView entropy, int64_t vocab_start, int64_t ignore_index,
double inverse_temperature, int64_t team_handle, int64_t tiles_per_reduce,
bool return_entropy, ffi::TensorView grad_input, ffi::TensorView grad_weight) {
DLDataType bf16{kDLBfloat, 16, 1};
DLDataType f32{kDLFloat, 32, 1};
DLDataType i64{kDLInt, 64, 1};
RequireCudaTensor(grad_output, 1, f32, "grad_output");
RequireCudaTensor(entropy_grad, 1, f32, "entropy_grad");
RequireCudaTensor(x, 2, bf16, "x");
RequireCudaTensor(weight, 2, bf16, "weight");
RequireCudaTensor(target, 1, i64, "target");
RequireCudaTensor(lse, 1, f32, "lse");
RequireCudaTensor(entropy, 1, f32, "entropy");
RequireCudaTensor(grad_input, 2, bf16, "grad_input");
RequireCudaTensor(grad_weight, 2, bf16, "grad_weight");
RequireSameCudaDevice(entropy_grad, x, "entropy_grad");
RequireSameCudaDevice(weight, x, "weight");
RequireSameCudaDevice(target, x, "target");
RequireSameCudaDevice(grad_output, x, "grad_output");
RequireSameCudaDevice(lse, x, "lse");
RequireSameCudaDevice(entropy, x, "entropy");
RequireSameCudaDevice(grad_input, x, "grad_input");
RequireSameCudaDevice(grad_weight, x, "grad_weight");
int64_t tokens = x.size(0);
int64_t hidden = x.size(1);
int64_t local_vocab = weight.size(0);
TVM_FFI_ICHECK_EQ(weight.size(1), hidden);
TVM_FFI_ICHECK_EQ(target.size(0), tokens);
TVM_FFI_ICHECK_EQ(grad_output.size(0), tokens);
TVM_FFI_ICHECK_EQ(entropy_grad.size(0), tokens);
TVM_FFI_ICHECK_EQ(lse.size(0), tokens);
TVM_FFI_ICHECK_EQ(entropy.size(0), tokens);
TVM_FFI_ICHECK_EQ(grad_input.size(0), tokens);
TVM_FFI_ICHECK_EQ(grad_input.size(1), hidden);
TVM_FFI_ICHECK_EQ(grad_weight.size(0), local_vocab);
TVM_FFI_ICHECK_EQ(grad_weight.size(1), hidden);
TVM_FFI_ICHECK(
tiles_per_reduce == 1 || tiles_per_reduce == 2 || tiles_per_reduce == 4);
RequirePositiveInverseTemperature(inverse_temperature);
using namespace liger::fused_scaled_linear_cross_entropy;
cudaStream_t stream = reinterpret_cast<cudaStream_t>(
TVMFFIEnvGetStream(x.device().device_type, x.device().device_id));
try {
TVM_FFI_ICHECK_EQ(team_handle, backward_dx_team_handle())
<< "team_handle must match the configured tensor-parallel team";
BackwardScratch scratch = reserve_backward_scratch(static_cast<int>(local_vocab));
BackwardTpParamsSm90<90> params;
params.gemm.x = x.data_ptr();
params.gemm.weight = weight.data_ptr();
params.gemm.target = static_cast<const int64_t*>(target.data_ptr());
params.gemm.grad_output = static_cast<const float*>(grad_output.data_ptr());
params.gemm.lse = static_cast<const float*>(lse.data_ptr());
params.gemm.entropy = static_cast<const float*>(entropy.data_ptr());
params.gemm.entropy_grad = static_cast<const float*>(entropy_grad.data_ptr());
params.gemm.grad_input = grad_input.data_ptr();
params.gemm.grad_weight = grad_weight.data_ptr();
params.gemm.dz_workspace = scratch.dz_workspace;
params.gemm.dz_workspace_bytes = scratch.dz_workspace_bytes;
params.gemm.tokens = static_cast<int>(tokens);
params.gemm.hidden = static_cast<int>(hidden);
params.gemm.local_vocab = static_cast<int>(local_vocab);
params.gemm.vocab_start = vocab_start;
params.gemm.ignore_index = ignore_index;
params.gemm.inverse_temperature = static_cast<float>(inverse_temperature);
params.tiles_per_reduce = static_cast<int>(tiles_per_reduce);
params.num_comm_channels = 1;
if (return_entropy) {
liger::fused_scaled_linear_cross_entropy::
fused_linear_scaled_cross_entropy_backward<true, 90>(params, stream);
} else {
liger::fused_scaled_linear_cross_entropy::
fused_linear_scaled_cross_entropy_backward<false, 90>(params, stream);
}
} catch (const std::exception& e) {
ThrowCoreError("fused_linear_scaled_cross_entropy_backward", e);
}
}
} // namespace
TVM_FFI_DLL_EXPORT_TYPED_FUNC(uniqueid_nbytes, uniqueid_nbytes);
@@ -391,6 +638,10 @@ TVM_FFI_DLL_EXPORT_TYPED_FUNC(get_uniqueid, get_uniqueid);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(init_with_uniqueid, init_with_uniqueid);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(init_pmi, init_pmi);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(finalize, finalize);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(nccl_unique_id_nbytes, nccl_unique_id_nbytes);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(nccl_get_unique_id, nccl_get_unique_id);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(nccl_comm_init_rank, nccl_comm_init_rank);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(nccl_comm_destroy, nccl_comm_destroy);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(my_pe, my_pe);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(n_pes, n_pes);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(team_world, team_world);
@@ -406,3 +657,15 @@ TVM_FFI_DLL_EXPORT_TYPED_FUNC(moe_configure_symmetric, moe_configure_symmetric);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(moe_pop_fwd, moe_pop_fwd);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(moe_fused_fwd_bf16, moe_fused_fwd_bf16);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(moe_fused_bwd_bf16, moe_fused_bwd_bf16);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(
fused_linear_scaled_cross_entropy_configure_forward,
fused_linear_scaled_cross_entropy_configure_forward);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(
fused_linear_scaled_cross_entropy_configure_backward,
fused_linear_scaled_cross_entropy_configure_backward);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(
fused_linear_scaled_cross_entropy_forward,
fused_linear_scaled_cross_entropy_forward);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(
fused_linear_scaled_cross_entropy_backward,
fused_linear_scaled_cross_entropy_backward);
+282
View File
@@ -0,0 +1,282 @@
from __future__ import annotations
import os
import shutil
import sys
import tempfile
from datetime import timedelta
import pytest
try:
import torch
import torch.distributed as dist
import torch.multiprocessing as mp
from liger_cute_kernels import nvshmem
from liger_cute_kernels import tvm_ffi
except ImportError:
torch = None
dist = None
mp = None
nvshmem = None
tvm_ffi = None
pytestmark = pytest.mark.skipif(
nvshmem is None,
reason="liger_cute_kernels dependencies are not installed",
)
_NATIVE_AVAILABLE = tvm_ffi is not None and tvm_ffi.is_available()
_NDEV = torch.cuda.device_count() if _NATIVE_AVAILABLE and torch is not None and torch.cuda.is_available() else 0
def test_fslce_forward_binding_is_exposed():
if not _NATIVE_AVAILABLE:
pytest.skip("requires the native core")
module = tvm_ffi._load_module()
for name in (
"nccl_get_unique_id",
"nccl_comm_init_rank",
"fused_linear_scaled_cross_entropy_configure_forward",
"fused_linear_scaled_cross_entropy_forward",
):
assert hasattr(module, name), f"missing native TVM FFI export: {name}"
def test_forward_returns_zero_entropy_when_disabled(monkeypatch):
class FakeModule:
@staticmethod
def fused_linear_scaled_cross_entropy_forward(*args):
del args
monkeypatch.setattr(tvm_ffi, "_load_module", lambda: FakeModule())
x = torch.empty((2, 8), dtype=torch.bfloat16)
weight = torch.empty((4, 8), dtype=torch.bfloat16)
target = torch.zeros(2, dtype=torch.int64)
_, _, entropy = tvm_ffi.fused_linear_scaled_cross_entropy_forward(
x,
weight,
target,
vocab_start=0,
ignore_index=-100,
inverse_temperature=1.0,
nccl_comm_handle=1,
return_entropy=False,
)
torch.testing.assert_close(entropy, torch.zeros_like(entropy))
def _reference(x, weights, target, grad_output, temperature):
logits = (x.float() @ weights.float().T) / temperature
lse = torch.logsumexp(logits, dim=-1)
log_probs = logits - lse[:, None]
probabilities = torch.softmax(logits, dim=-1)
rows = torch.arange(x.shape[0], device=x.device)
nll = -log_probs[rows, target]
entropy = -(probabilities * log_probs).sum(dim=-1)
dz = probabilities
dz[torch.arange(x.shape[0], device=x.device), target] -= 1.0
dz *= grad_output[:, None] / temperature
dz = dz.to(torch.bfloat16).float()
return nll, lse, entropy, dz @ weights.float(), dz
def _worker(rank: int, world_size: int, init_file: str, subgroup: bool):
torch.cuda.set_device(rank)
dist.init_process_group(
backend="nccl",
init_method=f"file://{init_file}",
rank=rank,
world_size=world_size,
timeout=timedelta(seconds=180),
)
team = None
nccl_comm = 0
try:
nvshmem.init_from_pg()
pg = dist.group.WORLD
group_ranks = list(range(world_size))
if subgroup:
groups = ([0, 2], [1, 3])
for ranks in groups:
candidate = dist.new_group(ranks=ranks)
if rank in ranks:
pg = candidate
group_ranks = list(ranks)
team = nvshmem.resolve_team(pg)
else:
team = nvshmem.resolve_team(None)
local_rank = group_ranks.index(rank)
team_size = len(group_ranks)
unique_id_payload = [tvm_ffi.nccl_get_unique_id().tolist() if local_rank == 0 else None]
dist.broadcast_object_list(
unique_id_payload,
src=group_ranks[0],
group=pg,
device=torch.device("cuda", rank),
)
unique_id = torch.tensor(unique_id_payload[0], dtype=torch.uint8)
nccl_comm = tvm_ffi.nccl_comm_init_rank(local_rank, team_size, unique_id)
tokens = 128
hidden = 2048
local_vocab = 320
temperature = 0.8
global_vocab = team_size * local_vocab
generator = torch.Generator(device="cpu")
generator.manual_seed(2027 + (rank & 1 if subgroup else 0))
x = torch.randn(tokens, hidden, generator=generator).mul_(0.05).to(device="cuda", dtype=torch.bfloat16)
weight_generator = torch.Generator(device="cpu")
weight_generator.manual_seed(3100 + local_rank)
weight = (
torch.randn(local_vocab, hidden, generator=weight_generator)
.mul_(0.05)
.to(device="cuda", dtype=torch.bfloat16)
)
gathered = [torch.empty_like(weight) for _ in range(team_size)]
dist.all_gather(gathered, weight, group=pg)
global_weight = torch.cat(gathered, dim=0)
target = torch.arange(tokens, device="cuda", dtype=torch.int64) % global_vocab
grad_output = torch.linspace(0.5, 1.0, tokens, device="cuda", dtype=torch.float32)
entropy = torch.zeros(tokens, device="cuda", dtype=torch.float32)
entropy_grad = torch.zeros_like(entropy)
expected_nll, expected_lse, expected_entropy, expected_dx, dz = _reference(
x, global_weight, target, grad_output, temperature
)
expected_dw = dz[:, local_rank * local_vocab : (local_rank + 1) * local_vocab].T @ x.float()
tvm_ffi.fused_linear_scaled_cross_entropy_configure_forward(tokens, local_vocab)
tvm_ffi.fused_linear_scaled_cross_entropy_configure_backward(tokens, hidden, local_vocab, 1, team)
actual_nll, lse, entropy = tvm_ffi.fused_linear_scaled_cross_entropy_forward(
x,
weight,
target,
local_rank * local_vocab,
-100,
1.0 / temperature,
nccl_comm,
True,
)
actual_dx, actual_dw = tvm_ffi.fused_linear_scaled_cross_entropy_backward(
grad_output,
entropy_grad,
x,
weight,
target,
lse,
entropy,
local_rank * local_vocab,
-100,
1.0 / temperature,
team,
1,
False,
)
torch.cuda.synchronize()
torch.testing.assert_close(actual_nll, expected_nll, atol=2e-4, rtol=2e-4)
torch.testing.assert_close(lse, expected_lse, atol=2e-4, rtol=2e-4)
torch.testing.assert_close(entropy, expected_entropy, atol=2e-4, rtol=2e-4)
torch.testing.assert_close(actual_dx.float(), expected_dx, atol=8e-3, rtol=4e-2)
torch.testing.assert_close(actual_dw.float(), expected_dw, atol=8e-3, rtol=4e-2)
nvshmem.pool_clear_all()
tvm_ffi.fused_linear_scaled_cross_entropy_configure_backward(tokens, hidden, local_vocab, 1, team)
replay_dx = torch.empty_like(x)
replay_dw = torch.empty_like(weight)
module = tvm_ffi._load_module()
graph = torch.cuda.CUDAGraph()
dist.barrier()
with torch.cuda.graph(graph):
module.fused_linear_scaled_cross_entropy_backward(
grad_output,
entropy_grad,
x,
weight,
target,
lse,
entropy,
local_rank * local_vocab,
-100,
1.0 / temperature,
team,
1,
False,
replay_dx,
replay_dw,
)
dist.barrier()
graph.replay()
torch.cuda.synchronize()
torch.testing.assert_close(replay_dx.float(), expected_dx, atol=8e-3, rtol=4e-2)
torch.testing.assert_close(replay_dw.float(), expected_dw, atol=8e-3, rtol=4e-2)
replay_dx.fill_(float("nan"))
replay_dw.fill_(float("nan"))
torch.cuda.synchronize()
dist.barrier()
graph.replay()
torch.cuda.synchronize()
torch.testing.assert_close(replay_dx.float(), expected_dx, atol=8e-3, rtol=4e-2)
torch.testing.assert_close(replay_dw.float(), expected_dw, atol=8e-3, rtol=4e-2)
tvm_ffi.nccl_comm_destroy(nccl_comm)
nccl_comm = 0
nvshmem.pool_clear_all()
if subgroup and team != nvshmem.team_world():
nvshmem.team_destroy(team)
team = None
dist.barrier()
nvshmem.finalize()
dist.destroy_process_group()
except BaseException:
if nccl_comm:
try:
tvm_ffi.nccl_comm_destroy(nccl_comm)
except Exception as cleanup_error:
print(f"NCCL communicator cleanup failed: {cleanup_error}", file=sys.stderr)
try:
dist.destroy_process_group()
except Exception:
pass
raise
def _run(world_size: int, subgroup: bool):
rendezvous = tempfile.mkdtemp(prefix="fslce_team_")
init_file = os.path.join(rendezvous, "store")
old_heap = os.environ.get("NVSHMEM_SYMMETRIC_SIZE")
os.environ["NVSHMEM_SYMMETRIC_SIZE"] = "3G"
try:
mp.spawn(
_worker,
args=(world_size, init_file, subgroup),
nprocs=world_size,
join=True,
)
finally:
if old_heap is None:
os.environ.pop("NVSHMEM_SYMMETRIC_SIZE", None)
else:
os.environ["NVSHMEM_SYMMETRIC_SIZE"] = old_heap
shutil.rmtree(rendezvous, ignore_errors=True)
@pytest.mark.skipif(
not _NATIVE_AVAILABLE or _NDEV < 2,
reason="requires the native core and at least two GPUs",
)
def test_fslce_forward_backward_world_team():
_run(2, subgroup=False)
@pytest.mark.skipif(
not _NATIVE_AVAILABLE or _NDEV < 4,
reason="requires the native core and at least four GPUs",
)
def test_fslce_forward_backward_process_group_subteams():
_run(4, subgroup=True)
@@ -27,6 +27,20 @@ target_include_directories(test_mlp1_fused PRIVATE
"${CMAKE_SOURCE_DIR}/csrc/core/src/moe")
gtest_discover_tests(test_mlp1_fused DISCOVERY_MODE PRE_TEST)
add_executable(
test_fused_scaled_linear_cross_entropy
test_fused_scaled_linear_cross_entropy.cu)
target_link_libraries(test_fused_scaled_linear_cross_entropy PRIVATE
GTest::gtest
CUTLASS::CUTLASS
CUDA::cudart)
target_include_directories(test_fused_scaled_linear_cross_entropy PRIVATE
"${CMAKE_SOURCE_DIR}/csrc/core/include"
"${CMAKE_SOURCE_DIR}/csrc/core/src/fused_scaled_linear_cross_entropy")
gtest_discover_tests(
test_fused_scaled_linear_cross_entropy
DISCOVERY_MODE PRE_TEST)
# ── Blackwell (SM100a) port targets — pre-staged by the orchestrator so the
# three parallel port subagents each build ONLY their own already-registered
# target and never edit this shared file. Each test .cu starts as a minimal stub
@@ -0,0 +1,195 @@
#include <gtest/gtest.h>
#include <cuda_runtime.h>
#include <cmath>
#include <cstdint>
#include "backward_gemm_mainloop_sm90.cuh"
#include "fused_scaled_linear_cross_entropy.cuh"
namespace fslce = liger::fused_scaled_linear_cross_entropy;
using ForwardConfig = fslce::ForwardGemmConfigSm90<90>;
using BackwardConfig = fslce::BackwardGemmConfigSm90<90>;
using CommConfig = fslce::DxCommConfig<BackwardConfig, 4, 2>;
static_assert(ForwardConfig::kCompute == 90);
static_assert(BackwardConfig::kCompute == 90);
static_assert(fslce::BackwardDzHandoffTraitsSm90<90>::kStages == 3);
static_assert(fslce::BackwardDxCluster2TraitsSm90<90>::kStages == 4);
static_assert(fslce::BackwardDxCluster2TraitsSm90<90>::kStoreTileN == 32);
static_assert(sizeof(fslce::BackwardDxCluster2SmemSm90<90>) == 230400);
static_assert(sizeof(fslce::DxReduceWorkspace<float>) == 4 * sizeof(void*));
static_assert(sizeof(liger_cute::detail::NvlsReduceView) <= 5 * sizeof(void*));
static_assert(
sizeof(liger_cute::detail::DirectPeerReduceView) <= 4 * sizeof(void*));
static_assert(fslce::backward_dx_split_k(2048, 256) == 2);
static_assert(fslce::backward_dx_split_k(2048, 320) == 1);
static_assert(fslce::backward_dx_split_k(4096, 256) == 1);
TEST(TensorParallelFusedScaledLinearCrossEntropyConfig, PreservesHopperWarpRoles) {
EXPECT_EQ(
fslce::backward_warp_role(0),
fslce::BackwardWarpRole::kProducer);
EXPECT_EQ(
fslce::backward_warp_role(1),
fslce::BackwardWarpRole::kDxCommunication);
EXPECT_EQ(
fslce::backward_warp_role(2),
fslce::BackwardWarpRole::kDxCommunication);
EXPECT_EQ(
fslce::backward_warp_role(3),
fslce::BackwardWarpRole::kReserved);
for (int warp = 4; warp < fslce::kNumWarps; ++warp) {
EXPECT_EQ(
fslce::backward_warp_role(warp),
fslce::BackwardWarpRole::kConsumer);
}
EXPECT_EQ(
fslce::backward_warp_role(fslce::kNumWarps),
fslce::BackwardWarpRole::kInactive);
}
TEST(TensorParallelFusedScaledLinearCrossEntropyConfig, CoalescesTwoDxTilesByDefault) {
EXPECT_EQ(fslce::dx_groups_per_m_tile<CommConfig>(5), 3);
fslce::DxTileGroup first =
fslce::make_dx_tile_group<CommConfig>(3, 0, 5, 7);
EXPECT_EQ(first.m_tile, 3);
EXPECT_EQ(first.first_n_tile, 0);
EXPECT_EQ(first.num_tiles, 2);
EXPECT_EQ(first.sequence, 7);
fslce::DxTileGroup tail =
fslce::make_dx_tile_group<CommConfig>(3, 2, 5, 9);
EXPECT_EQ(tail.first_n_tile, 4);
EXPECT_EQ(tail.num_tiles, 1);
}
TEST(TensorParallelFusedScaledLinearCrossEntropyOnlineSoftmax, MatchesGlobalReference) {
fslce::OnlineSoftmaxState rank0;
rank0.fold<true>(1.0f, true, false);
rank0.fold<true>(3.0f, true, false);
fslce::OnlineSoftmaxState rank1;
rank1.fold<true>(2.0f, true, true);
rank1.fold<true>(4.0f, true, false);
float global_max = fmaxf(rank0.max_value, rank1.max_value);
fslce::CorrectedSoftmaxStats corrected0 =
fslce::correct_for_global_max<true>(rank0, global_max);
fslce::CorrectedSoftmaxStats corrected1 =
fslce::correct_for_global_max<true>(rank1, global_max);
float global_sum = corrected0.exp_sum + corrected1.exp_sum;
float global_weighted =
corrected0.exp_weighted_sum + corrected1.exp_weighted_sum;
float global_target =
corrected0.target_logit + corrected1.target_logit;
fslce::FinalizedSoftmax result = fslce::finalize_softmax<true>(
global_max,
global_sum,
global_target,
global_weighted,
false);
float direct_sum =
expf(1.0f - global_max) +
expf(2.0f - global_max) +
expf(3.0f - global_max) +
expf(4.0f - global_max);
float expected_lse = global_max + logf(direct_sum);
float direct_weighted =
expf(1.0f - global_max) * 1.0f +
expf(2.0f - global_max) * 2.0f +
expf(3.0f - global_max) * 3.0f +
expf(4.0f - global_max) * 4.0f;
EXPECT_NEAR(result.lse, expected_lse, 1.0e-6f);
EXPECT_NEAR(result.nll, expected_lse - 2.0f, 1.0e-6f);
EXPECT_NEAR(
result.entropy,
expected_lse - direct_weighted / direct_sum,
1.0e-6f);
}
TEST(TensorParallelFusedScaledLinearCrossEntropyOnlineSoftmax, ChunkMergeMatchesSingleFold) {
fslce::OnlineSoftmaxState lhs;
lhs.fold<true>(-2.0f, true, false);
lhs.fold<true>(0.5f, true, false);
fslce::OnlineSoftmaxState rhs;
rhs.fold<true>(3.0f, true, true);
rhs.fold<true>(1.0f, true, false);
fslce::OnlineSoftmaxState merged =
fslce::merge_online_softmax<true>(lhs, rhs);
fslce::OnlineSoftmaxState direct;
direct.fold<true>(-2.0f, true, false);
direct.fold<true>(0.5f, true, false);
direct.fold<true>(3.0f, true, true);
direct.fold<true>(1.0f, true, false);
EXPECT_NEAR(merged.max_value, direct.max_value, 1.0e-7f);
EXPECT_NEAR(merged.exp_sum, direct.exp_sum, 1.0e-6f);
EXPECT_NEAR(
merged.exp_weighted_sum,
direct.exp_weighted_sum,
1.0e-6f);
EXPECT_EQ(merged.has_target, 1);
EXPECT_FLOAT_EQ(merged.target_logit, 3.0f);
}
TEST(TensorParallelFusedScaledLinearCrossEntropyOnlineSoftmax, CompilesOutEntropyMoment) {
fslce::OnlineSoftmaxState state;
state.fold<false>(2.0f, true, false);
state.fold<false>(4.0f, true, false);
EXPECT_FLOAT_EQ(state.exp_weighted_sum, 0.0f);
}
TEST(TensorParallelFusedScaledLinearCrossEntropyOnlineSoftmax, ZerosIgnoredEntropy) {
fslce::FinalizedSoftmax result = fslce::finalize_softmax<true>(
4.0f,
2.0f,
0.0f,
6.0f,
true);
EXPECT_FLOAT_EQ(result.nll, 0.0f);
EXPECT_FLOAT_EQ(result.entropy, 0.0f);
}
TEST(TensorParallelFusedScaledLinearCrossEntropyBackward, AppliesTemperatureChainRule) {
float temperature = 2.0f;
float scaled_logit = 0.25f;
float lse = 1.25f;
float scale = 0.75f;
float probability = expf(scaled_logit - lse);
float gradient = fslce::scaled_cross_entropy_dlogit(
scaled_logit,
lse,
true,
scale,
temperature);
float expected = scale * (probability - 1.0f) / temperature;
EXPECT_NEAR(gradient, expected, 1.0e-7f);
}
TEST(TensorParallelFusedScaledLinearCrossEntropyBackward, ZerosIgnoredRows) {
float gradient = fslce::scaled_cross_entropy_dlogit(
0.25f,
1.25f,
true,
1.0f,
2.0f,
0.5f,
1.0f,
true);
EXPECT_FLOAT_EQ(gradient, 0.0f);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}