[Megatron] Add cross-entropy integration (#1207)

## Summary

This PR adds Liger's Triton cross-entropy kernel to Megatron-LM.

Megatron has two cross-entropy code paths. This PR replaces both with
Liger:

- The **fused** path: `fused_vocab_parallel_cross_entropy` (used when
`cross_entropy_loss_fusion=True`).
- The **unfused** path: `vocab_parallel_cross_entropy` (used when
`cross_entropy_loss_fusion=False`).

You get Liger's kernel no matter which path your Megatron config picks.

Liger's kernel uses online softmax. It writes gradients in place. It
never builds the full softmax tensor. The net effect is faster training
and lower memory use.

This PR also keeps the RMSNorm patch that was added in PR #1254. So one
call patches both kernels at once.

## How to use

There are two ways. Pick whichever fits your code better.

### Mode 1: One-line monkey patch

Best for users who don't want to touch their model code. Call this once
before you build the model:

```python
from liger_kernel.megatron import apply_liger_kernel_to_megatron

apply_liger_kernel_to_megatron(rms_norm=True, cross_entropy=True)
```

That's the whole API. No CE-specific knobs. The defaults match
Megatron's native behavior.

### Mode 2: Use the classes directly

Best for users who build their model spec by hand or need custom config.
Import the classes and wire them in yourself:

```python
from liger_kernel.megatron import LigerMegatronRMSNorm, LigerMegatronCrossEntropy

# In your TransformerBlockSubmodules:
input_layernorm = LigerMegatronRMSNorm
pre_mlp_layernorm = LigerMegatronRMSNorm

# In a GPTModel subclass:
class MyModel(GPTModel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.liger_ce = LigerMegatronCrossEntropy(
            ignore_index=-100,
            label_smoothing=0.1,
        )

    def compute_language_model_loss(self, labels, logits):
        labels_sb = labels.transpose(0, 1).contiguous()
        loss_sb = self.liger_ce(logits, labels_sb, self.pg_collection.tp)
        return loss_sb.transpose(0, 1).contiguous()
```

The class accepts the same knobs Liger's standalone CE accepts
(`ignore_index`, `label_smoothing`, etc.). Use Mode 2 when you need
them.

## Scope

This PR supports `tensor_model_parallel_size=1` only.

Why? At TP>1, each GPU holds a slice of the vocabulary, not the full
vocab. Cross-entropy then needs cross-rank all-reduces to get a correct
loss. Liger's kernel does not do those all-reduces. So the math would be
wrong at TP>1.

To stop you from training with a broken loss, the patch raises
`RuntimeError` if it sees TP>1. The check runs at patch time and again
at call time, so misconfigurations fail loudly.

**[TODO]**: TP>1 support is planned as follow-up work (a separate
"vocab-parallel" Triton kernel).

You can still use this PR with pipeline parallelism, data parallelism,
or single-GPU runs. Those are all TP=1 cases.


## Testing

All unit tests live in `test/megatron/`.

### Run all megatron tests

```bash
python -m pytest test/megatron/ --no-cov
```

That runs three files:

- `test/megatron/test_rms_norm.py` — RMSNorm class correctness (forward,
backward, dtypes).
- `test/megatron/test_cross_entropy.py` — `LigerMegatronCrossEntropy`
class correctness. Covers shape sweeps, `ignore_index`,
`label_smoothing`, combined config, out-of-bounds targets,
`torch.no_grad()` path, and gradient parity vs. PyTorch's
`F.cross_entropy`.
- `test/megatron/test_monkey_patch.py` —
`apply_liger_kernel_to_megatron()` plumbing. Covers symbol replacement
for both CE paths and both RMSNorm targets, idempotency,
missing-megatron errors, TP>1 guard, dispatch behavior, and end-to-end
correctness through the patched symbols.

You should see all tests pass (currently 105 tests in the megatron
suite).

### Run a single test file

```bash
python -m pytest test/megatron/test_cross_entropy.py -v --no-cov
```

### What the tests do NOT need

Megatron-LM is **not** a test dependency. The monkey-patch tests use
stub modules in `sys.modules`, so they run on CPU without a real
Megatron install. You only need PyTorch + Triton + Liger itself.

The correctness tests (RMSNorm, CE) need a GPU because they run the
actual Triton kernel.

## Benchmarking

### Step 1: Run the benchmark

```bash
python benchmark/scripts/benchmark_megatron_cross_entropy.py --overwrite
```

This compares four providers across vocab sizes 4K → 128K (with S=2048,
B=4, BF16):

- **liger** — `LigerMegatronCrossEntropy`
- **torch** — vanilla `F.cross_entropy`
- **megatron** — Megatron's fused CE (`@jit_fuser` via TorchScript)
- **megatron-unfused** — Megatron's unfused CE (eager Python)

The script needs a CUDA/ROCm GPU. If Megatron-LM is not installed, the
two megatron providers are silently skipped and you only get `liger` vs
`torch`.

Results land in `benchmark/data/all_benchmark_data.csv`. Each row is
tagged with `kernel_name="megatron_cross_entropy"`.

### Step 2: Render plots

```bash
python benchmark/benchmarks_visualizer.py \
    --kernel-name megatron_cross_entropy --metric-name speed

python benchmark/benchmarks_visualizer.py \
    --kernel-name megatron_cross_entropy --metric-name memory
```

PNGs land in `benchmark/visualizations/`.

### Memory savings
<img width="1000" height="600"
alt="megatron_cross_entropy_memory_full_token_length"
src="https://github.com/user-attachments/assets/d08d7ee7-f6de-4e8e-a548-98b50da066ea"
/>

### Backward speedup
<img width="1000" height="600"
alt="megatron_cross_entropy_speed_backward_token_length"
src="https://github.com/user-attachments/assets/f8b524e2-658f-4206-b5a5-9d4d5e52e816"
/>

### Forward speedup
<img width="1000" height="600"
alt="megatron_cross_entropy_speed_forward_token_length"
src="https://github.com/user-attachments/assets/771115ff-143a-4c63-b112-c50bc0dddd84"
/>

### Backward+Forward speedup
<img width="1000" height="600"
alt="megatron_cross_entropy_speed_full_token_length"
src="https://github.com/user-attachments/assets/2152656c-c13e-4a6f-8b3d-577d6365a26e"
/>


## Example training scripts

Two end-to-end examples in `examples/megatron/`. Both train a tiny GPT
model with mock data for 5 iterations. They print which classes filled
which norm slots and which function backs each CE entry point, so you
can see Liger took over.

### Prerequisites

- A working Megatron-Core install (`pip install megatron-core`).
- `liger-kernel` installed.
- `psutil` (Megatron's async checkpoint worker pool uses it).
- At least 2 GPUs.

### Example 1: Mode 1 (monkey patch)

```bash
torchrun --nproc_per_node=2 \
    --master_addr=127.0.0.1 --master_port=29500 \
    examples/megatron/run_mode1_monkey_patch.py
```

One `apply_liger_kernel_to_megatron(rms_norm=True, cross_entropy=True)`
call at the top. Everything else is stock Megatron code.

### Example 2: Mode 2 (hand-built spec)

```bash
torchrun --nproc_per_node=2 \
    --master_addr=127.0.0.1 --master_port=29500 \
    examples/megatron/run_mode2_hand_spec.py
```

No monkey patch. The script builds a `TransformerBlockSubmodules` with
`LigerMegatronRMSNorm` slots and subclasses `GPTModel` to use
`LigerMegatronCrossEntropy` in `compute_language_model_loss`.

### What you should see

For both:

- 5 lines of `[modeN] iter <i> loss=<float>` with the loss going down.
- A printed module tree with `LigerMegatronRMSNorm` in 5 of 5 norm slots
(4 per-layer + 1 block-level final).
- The CE symbols print as `liger_fused_vocab_parallel_cross_entropy` and
`liger_vocab_parallel_cross_entropy` (Mode 1) or
`LigerMegatronCrossEntropy` (Mode 2).
- `Successfully loaded the model` after a distributed checkpoint
round-trip.

If Apex or TransformerEngine isn't installed, you'll see harmless
warnings. Megatron falls back to the local backend, which is where Liger
plugs in.

## Testing Done

- Hardware Type: H100
- [x] run `make test` to ensure correctness
- [x] run `make checkstyle` to ensure code style
- [ ] run `make test-convergence` to ensure convergence

---------

Co-authored-by: Vaibhav Jindal <[email protected]>
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
Prathyusha
2026-06-10 20:51:13 +00:00
committed by GitHub
co-authored by Vaibhav Jindal Claude Opus 4.7
parent e06ee2ce44
commit 0400d31357
10 changed files with 2205 additions and 36 deletions
+96
View File
@@ -2219,3 +2219,99 @@ jsd,liger,full,memory,MB,BT,total tokens,1024,3514.0009765625,3514.0009765625,35
jsd,liger,full,memory,MB,BT,total tokens,2048,7014.0009765625,7014.0009765625,7014.0009765625,"{""vocab_size"": 128256, ""bsz"": 1, ""seq_len"": 8192}",NVIDIA B200,2026-05-27 17:14:32,0.8.0
jsd,liger,full,memory,MB,BT,total tokens,4096,14028.0009765625,14028.0009765625,14028.0009765625,"{""vocab_size"": 128256, ""bsz"": 1, ""seq_len"": 8192}",NVIDIA B200,2026-05-27 17:14:32,0.8.0
jsd,liger,full,memory,MB,BT,total tokens,8192,28056.0,28056.0,28056.0,"{""vocab_size"": 128256, ""bsz"": 1, ""seq_len"": 8192}",NVIDIA B200,2026-05-27 17:14:32,0.8.0
megatron_cross_entropy,liger,forward,speed,ms,V,vocab size,4096,0.2385600060224533,0.23590399324893951,0.24048000574111938,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:43:58,0.8.0
megatron_cross_entropy,liger,forward,speed,ms,V,vocab size,8192,0.26820799708366394,0.26531198620796204,0.27125120162963867,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:43:58,0.8.0
megatron_cross_entropy,liger,forward,speed,ms,V,vocab size,16384,0.3599199950695038,0.3569599986076355,0.36266239881515505,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:43:58,0.8.0
megatron_cross_entropy,liger,forward,speed,ms,V,vocab size,32768,0.6502079963684082,0.6452223896980286,0.6563008189201356,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:43:58,0.8.0
megatron_cross_entropy,liger,forward,speed,ms,V,vocab size,65536,1.087440013885498,1.0827391862869262,1.0942399978637696,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:43:58,0.8.0
megatron_cross_entropy,liger,forward,speed,ms,V,vocab size,131072,2.1112000942230225,2.108524799346924,2.1132928848266603,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:43:58,0.8.0
megatron_cross_entropy,torch,forward,speed,ms,V,vocab size,4096,0.2269120067358017,0.225913605093956,0.22775039970874789,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:43:59,0.8.0
megatron_cross_entropy,torch,forward,speed,ms,V,vocab size,8192,0.48070400953292847,0.47954559326171875,0.48152959942817686,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:43:59,0.8.0
megatron_cross_entropy,torch,forward,speed,ms,V,vocab size,16384,0.8124480247497559,0.8111680150032043,0.813696026802063,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:43:59,0.8.0
megatron_cross_entropy,torch,forward,speed,ms,V,vocab size,32768,2.1381120681762695,2.1316160202026366,2.142361545562744,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:43:59,0.8.0
megatron_cross_entropy,torch,forward,speed,ms,V,vocab size,65536,4.391056060791016,4.389933013916016,4.3939519882202145,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:43:59,0.8.0
megatron_cross_entropy,torch,forward,speed,ms,V,vocab size,131072,8.81107234954834,8.806303977966309,8.81488037109375,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:43:59,0.8.0
megatron_cross_entropy,megatron,forward,speed,ms,V,vocab size,4096,0.31273600459098816,0.31200000643730164,0.31331199407577515,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:01,0.8.0
megatron_cross_entropy,megatron,forward,speed,ms,V,vocab size,8192,0.6373440027236938,0.636352002620697,0.6381440162658691,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:01,0.8.0
megatron_cross_entropy,megatron,forward,speed,ms,V,vocab size,16384,1.3848639726638794,1.3825664043426515,1.3870784282684325,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:01,0.8.0
megatron_cross_entropy,megatron,forward,speed,ms,V,vocab size,32768,2.829935908317566,2.8288448333740233,2.8316224098205565,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:01,0.8.0
megatron_cross_entropy,megatron,forward,speed,ms,V,vocab size,65536,5.823584079742432,5.8137922286987305,5.826655864715576,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:01,0.8.0
megatron_cross_entropy,megatron,forward,speed,ms,V,vocab size,131072,11.810944080352783,11.80013427734375,11.817446517944337,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:01,0.8.0
megatron_cross_entropy,megatron-unfused,forward,speed,ms,V,vocab size,4096,0.5649279952049255,0.5639231920242309,0.5661375880241394,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:02,0.8.0
megatron_cross_entropy,megatron-unfused,forward,speed,ms,V,vocab size,8192,1.094543993473053,1.093395209312439,1.0956799983978271,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:02,0.8.0
megatron_cross_entropy,megatron-unfused,forward,speed,ms,V,vocab size,16384,2.0701760053634644,2.068671941757202,2.072096109390259,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:02,0.8.0
megatron_cross_entropy,megatron-unfused,forward,speed,ms,V,vocab size,32768,4.027215957641602,4.024915313720703,4.028927993774413,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:02,0.8.0
megatron_cross_entropy,megatron-unfused,forward,speed,ms,V,vocab size,65536,7.93833589553833,7.937132930755616,7.940275096893311,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:02,0.8.0
megatron_cross_entropy,megatron-unfused,forward,speed,ms,V,vocab size,131072,15.817471981048584,15.814432144165039,15.821215629577637,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:02,0.8.0
megatron_cross_entropy,liger,backward,speed,ms,V,vocab size,4096,0.3516159951686859,0.3489919900894165,0.35476480722427367,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:03,0.8.0
megatron_cross_entropy,liger,backward,speed,ms,V,vocab size,8192,0.4639679938554764,0.46081281304359434,0.46724479794502255,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:03,0.8.0
megatron_cross_entropy,liger,backward,speed,ms,V,vocab size,16384,0.7123200297355652,0.7092544078826905,0.7154880046844483,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:03,0.8.0
megatron_cross_entropy,liger,backward,speed,ms,V,vocab size,32768,1.322975993156433,1.3184319734573364,1.328320026397705,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:03,0.8.0
megatron_cross_entropy,liger,backward,speed,ms,V,vocab size,65536,2.3985120058059692,2.393356847763062,2.405248022079468,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:03,0.8.0
megatron_cross_entropy,liger,backward,speed,ms,V,vocab size,131072,4.698495864868164,4.69042558670044,4.70184965133667,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:03,0.8.0
megatron_cross_entropy,torch,backward,speed,ms,V,vocab size,4096,0.48710399866104126,0.4862975895404816,0.48793599009513855,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:04,0.8.0
megatron_cross_entropy,torch,backward,speed,ms,V,vocab size,8192,0.9741439819335938,0.9731391787528991,0.9752448081970215,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:04,0.8.0
megatron_cross_entropy,torch,backward,speed,ms,V,vocab size,16384,1.8759199976921082,1.8737216234207152,1.8779136180877687,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:04,0.8.0
megatron_cross_entropy,torch,backward,speed,ms,V,vocab size,32768,4.405440092086792,4.398931121826172,4.407916831970215,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:04,0.8.0
megatron_cross_entropy,torch,backward,speed,ms,V,vocab size,65536,8.920191764831543,8.916671752929688,8.923616409301758,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:04,0.8.0
megatron_cross_entropy,torch,backward,speed,ms,V,vocab size,131072,17.845855712890625,17.843469619750977,17.847046661376954,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:04,0.8.0
megatron_cross_entropy,megatron,backward,speed,ms,V,vocab size,4096,0.5368639826774597,0.536191999912262,0.5374848127365113,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:05,0.8.0
megatron_cross_entropy,megatron,backward,speed,ms,V,vocab size,8192,1.0521279573440552,1.05141122341156,1.0528064489364624,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:05,0.8.0
megatron_cross_entropy,megatron,backward,speed,ms,V,vocab size,16384,2.1722079515457153,2.169472026824951,2.1735936641693114,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:05,0.8.0
megatron_cross_entropy,megatron,backward,speed,ms,V,vocab size,32768,4.448447942733765,4.447282981872559,4.449868965148926,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:05,0.8.0
megatron_cross_entropy,megatron,backward,speed,ms,V,vocab size,65536,9.033520221710205,9.030751991271973,9.042854690551758,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:05,0.8.0
megatron_cross_entropy,megatron,backward,speed,ms,V,vocab size,131072,17.958431243896484,17.9557315826416,17.960690689086913,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:05,0.8.0
megatron_cross_entropy,megatron-unfused,backward,speed,ms,V,vocab size,4096,0.7786880135536194,0.7767040133476257,0.7803391933441162,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:06,0.8.0
megatron_cross_entropy,megatron-unfused,backward,speed,ms,V,vocab size,8192,1.475823998451233,1.4742015838623046,1.4774847745895388,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:06,0.8.0
megatron_cross_entropy,megatron-unfused,backward,speed,ms,V,vocab size,16384,2.787328004837036,2.785683250427246,2.7883071899414062,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:06,0.8.0
megatron_cross_entropy,megatron-unfused,backward,speed,ms,V,vocab size,32768,5.420928001403809,5.419379329681396,5.423315238952636,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:06,0.8.0
megatron_cross_entropy,megatron-unfused,backward,speed,ms,V,vocab size,65536,10.677472114562988,10.674201774597169,10.67763843536377,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:06,0.8.0
megatron_cross_entropy,megatron-unfused,backward,speed,ms,V,vocab size,131072,21.254928588867188,21.25368995666504,21.2557315826416,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:06,0.8.0
megatron_cross_entropy,liger,full,speed,ms,V,vocab size,4096,0.41488000750541687,0.4122175931930542,0.41767039299011227,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:07,0.8.0
megatron_cross_entropy,liger,full,speed,ms,V,vocab size,8192,0.5923520028591156,0.5882560014724731,0.6005120277404785,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:07,0.8.0
megatron_cross_entropy,liger,full,speed,ms,V,vocab size,16384,0.9725440144538879,0.9690751910209655,0.9779008150100708,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:07,0.8.0
megatron_cross_entropy,liger,full,speed,ms,V,vocab size,32768,1.8396799564361572,1.8349119424819946,1.8454079627990723,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:07,0.8.0
megatron_cross_entropy,liger,full,speed,ms,V,vocab size,65536,3.4398880004882812,3.429087924957275,3.4599680423736574,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:07,0.8.0
megatron_cross_entropy,liger,full,speed,ms,V,vocab size,131072,6.780799865722656,6.763827323913574,6.796000003814697,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:07,0.8.0
megatron_cross_entropy,torch,full,speed,ms,V,vocab size,4096,0.5540960133075714,0.5528640151023865,0.555296003818512,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:08,0.8.0
megatron_cross_entropy,torch,full,speed,ms,V,vocab size,8192,1.1072640419006348,1.1059776306152345,1.1086400032043457,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:08,0.8.0
megatron_cross_entropy,torch,full,speed,ms,V,vocab size,16384,2.139024019241333,2.1362879276275635,2.142067241668701,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:08,0.8.0
megatron_cross_entropy,torch,full,speed,ms,V,vocab size,32768,4.9202880859375,4.91553258895874,4.924480152130127,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:08,0.8.0
megatron_cross_entropy,torch,full,speed,ms,V,vocab size,65536,9.961983680725098,9.958450889587402,9.964262008666992,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:08,0.8.0
megatron_cross_entropy,torch,full,speed,ms,V,vocab size,131072,19.926143646240234,19.913933563232423,19.93060417175293,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:08,0.8.0
megatron_cross_entropy,megatron,full,speed,ms,V,vocab size,4096,0.6057599782943726,0.604960024356842,0.6063359975814819,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:09,0.8.0
megatron_cross_entropy,megatron,full,speed,ms,V,vocab size,8192,1.1868000030517578,1.1857984066009521,1.1880639791488647,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:09,0.8.0
megatron_cross_entropy,megatron,full,speed,ms,V,vocab size,16384,2.4351680278778076,2.4330944538116457,2.4368255615234373,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:09,0.8.0
megatron_cross_entropy,megatron,full,speed,ms,V,vocab size,32768,4.968512058258057,4.967942237854004,4.969702434539795,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:09,0.8.0
megatron_cross_entropy,megatron,full,speed,ms,V,vocab size,65536,10.074239730834961,10.06367359161377,10.077676963806152,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:09,0.8.0
megatron_cross_entropy,megatron,full,speed,ms,V,vocab size,131072,20.022079467773438,20.015307998657228,20.02815971374512,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:09,0.8.0
megatron_cross_entropy,megatron-unfused,full,speed,ms,V,vocab size,4096,0.846015989780426,0.8447743892669677,0.8472639918327332,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,megatron-unfused,full,speed,ms,V,vocab size,8192,1.6103359460830688,1.6088960409164428,1.6117696285247802,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,megatron-unfused,full,speed,ms,V,vocab size,16384,3.0503358840942383,3.049056053161621,3.0524160861968994,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,megatron-unfused,full,speed,ms,V,vocab size,32768,5.937999963760376,5.936895847320557,5.939583778381348,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,megatron-unfused,full,speed,ms,V,vocab size,65536,11.711487770080566,11.710764503479005,11.713843536376952,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,megatron-unfused,full,speed,ms,V,vocab size,131072,23.322416305541992,23.321644973754882,23.32369956970215,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,liger,full,memory,MB,V,vocab size,4096,192.0791015625,192.0791015625,192.0791015625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,liger,full,memory,MB,V,vocab size,8192,384.0791015625,384.0791015625,384.0791015625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,liger,full,memory,MB,V,vocab size,16384,768.0791015625,768.0791015625,768.0791015625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,liger,full,memory,MB,V,vocab size,32768,1536.0791015625,1536.0791015625,1536.0791015625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,liger,full,memory,MB,V,vocab size,65536,3072.0791015625,3072.0791015625,3072.0791015625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,liger,full,memory,MB,V,vocab size,131072,6144.0791015625,6144.0791015625,6144.0791015625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,torch,full,memory,MB,V,vocab size,4096,512.0947265625,512.0947265625,512.0947265625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,torch,full,memory,MB,V,vocab size,8192,1024.0947265625,1024.0947265625,1024.0947265625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,torch,full,memory,MB,V,vocab size,16384,2048.0947265625,2048.0947265625,2048.0947265625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,torch,full,memory,MB,V,vocab size,32768,4096.0947265625,4096.0947265625,4096.0947265625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,torch,full,memory,MB,V,vocab size,65536,8192.0947265625,8192.0947265625,8192.0947265625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,torch,full,memory,MB,V,vocab size,131072,16384.09375,16384.09375,16384.09375,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,megatron,full,memory,MB,V,vocab size,4096,448.1650390625,448.1650390625,448.1650390625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,megatron,full,memory,MB,V,vocab size,8192,896.1650390625,896.1650390625,896.1650390625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,megatron,full,memory,MB,V,vocab size,16384,1792.1650390625,1792.1650390625,1792.1650390625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,megatron,full,memory,MB,V,vocab size,32768,3584.1650390625,3584.1650390625,3584.1650390625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,megatron,full,memory,MB,V,vocab size,65536,7168.1650390625,7168.1650390625,7168.1650390625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,megatron,full,memory,MB,V,vocab size,131072,14336.1650390625,14336.1650390625,14336.1650390625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:10,0.8.0
megatron_cross_entropy,megatron-unfused,full,memory,MB,V,vocab size,4096,320.1650390625,320.1650390625,320.1650390625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:11,0.8.0
megatron_cross_entropy,megatron-unfused,full,memory,MB,V,vocab size,8192,640.1650390625,640.1650390625,640.1650390625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:11,0.8.0
megatron_cross_entropy,megatron-unfused,full,memory,MB,V,vocab size,16384,1280.1650390625,1280.1650390625,1280.1650390625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:11,0.8.0
megatron_cross_entropy,megatron-unfused,full,memory,MB,V,vocab size,32768,2560.1650390625,2560.1650390625,2560.1650390625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:11,0.8.0
megatron_cross_entropy,megatron-unfused,full,memory,MB,V,vocab size,65536,5120.1650390625,5120.1650390625,5120.1650390625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:11,0.8.0
megatron_cross_entropy,megatron-unfused,full,memory,MB,V,vocab size,131072,10240.1650390625,10240.1650390625,10240.1650390625,"{""S"": 2048, ""B"": 4}",NVIDIA H100 80GB HBM3,2026-06-10 07:44:11,0.8.0
Can't render this file because it is too large.
@@ -0,0 +1,207 @@
"""Benchmark Liger's Megatron-LM cross-entropy wrapper.
Compares four providers on the per-token CE call shape ``[seq, batch, vocab]``:
- **torch**: vanilla ``F.cross_entropy``
- **megatron**: Megatron's *fused* ``fused_vocab_parallel_cross_entropy`` path
(``cross_entropy_loss_fusion=True``, JIT-fused via TorchScript)
- **megatron-unfused**: Megatron's *unfused* ``vocab_parallel_cross_entropy``
path (``cross_entropy_loss_fusion=False``, eager Python; the path users on
``label_smoothing`` typically end up on)
- **liger**: ``LigerMegatronCrossEntropy`` — Liger's Triton CE wrapped in the
Megatron fused signature. Same kernel regardless of which Megatron symbol
it was patched onto, so we only benchmark it once.
Requires a Liger-supported accelerator (CUDA / ROCm). With megatron-core not
installed, both megatron providers are silently skipped.
Output goes to the shared ``benchmark/data/all_benchmark_data.csv`` like every
other Liger benchmark — rows are tagged with ``kernel_name="megatron_cross_entropy"``
and the standard visualizer renders them via:
python benchmark/benchmarks_visualizer.py \\
--kernel-name megatron_cross_entropy --metric-name speed
python benchmark/benchmarks_visualizer.py \\
--kernel-name megatron_cross_entropy --metric-name memory
"""
import os
import torch
import torch.nn.functional as F
import triton
from utils import QUANTILES
from utils import SingleBenchmarkRunInput
from utils import SingleBenchmarkRunOutput
from utils import _test_memory
from utils import parse_benchmark_script_args
from utils import run_benchmarks
from liger_kernel.megatron import LigerMegatronCrossEntropy
from liger_kernel.utils import infer_device
device = infer_device()
try:
from megatron.core.fusions.fused_cross_entropy import fused_vocab_parallel_cross_entropy
from megatron.core.tensor_parallel.cross_entropy import vocab_parallel_cross_entropy
_MEGATRON_AVAILABLE = True
except ImportError:
fused_vocab_parallel_cross_entropy = None
vocab_parallel_cross_entropy = None
_MEGATRON_AVAILABLE = False
def _make_inputs(s: int, b: int, v: int, requires_grad: bool = True):
logits = torch.randn(s, b, v, device=device, dtype=torch.bfloat16, requires_grad=requires_grad)
target = torch.randint(0, v, (s, b), device=device, dtype=torch.long)
return logits, target
def _pytorch_cross_entropy(logits: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
s, b, v = logits.shape
return F.cross_entropy(
logits.reshape(-1, v).float(),
target.reshape(-1),
reduction="none",
).reshape(s, b)
def _ensure_single_rank_tp_group():
"""Initialize torch.distributed (single-rank) and return a usable TP group.
For a single-process benchmark we use the world group of size 1; the
internal all-reduce becomes a no-op.
"""
import torch.distributed as dist
if not dist.is_initialized():
os.environ.setdefault("MASTER_ADDR", "localhost")
os.environ.setdefault("MASTER_PORT", "29500")
os.environ.setdefault("WORLD_SIZE", "1")
os.environ.setdefault("RANK", "0")
os.environ.setdefault("LOCAL_RANK", "0")
dist.init_process_group(backend="nccl")
return dist.group.WORLD
def _select_fwd(provider: str):
if provider == "liger":
ce = LigerMegatronCrossEntropy(reduction="none")
return lambda logits, target: ce(logits, target)
if provider == "torch":
return _pytorch_cross_entropy
if provider == "megatron":
if not _MEGATRON_AVAILABLE:
raise RuntimeError("megatron-core not installed; cannot benchmark 'megatron' provider")
tp_group = _ensure_single_rank_tp_group()
def _megatron_fused_call(logits, target):
return fused_vocab_parallel_cross_entropy(logits, target, tp_group)
return _megatron_fused_call
if provider == "megatron-unfused":
if not _MEGATRON_AVAILABLE:
raise RuntimeError("megatron-core not installed; cannot benchmark 'megatron-unfused' provider")
tp_group = _ensure_single_rank_tp_group()
def _megatron_unfused_call(logits, target):
# Unfused signature: (logits, target, label_smoothing=0.0, tp_group=None)
return vocab_parallel_cross_entropy(logits, target, 0.0, tp_group)
return _megatron_unfused_call
raise ValueError(f"unknown provider: {provider!r}")
def bench_speed_megatron_cross_entropy(input: SingleBenchmarkRunInput) -> SingleBenchmarkRunOutput:
v = input.x
provider = input.kernel_provider
mode = input.kernel_operation_mode
s = input.extra_benchmark_config["S"]
b = input.extra_benchmark_config["B"]
logits, target = _make_inputs(s, b, v)
fwd_fn = _select_fwd(provider)
def fwd():
return fwd_fn(logits, target)
if mode == "forward":
ms_50, ms_20, ms_80 = triton.testing.do_bench(fwd, rep=100, quantiles=QUANTILES)
elif mode == "backward":
# Megatron's fused CE writes gradients in-place into saved tensors during backward,
# which breaks the standard retain_graph=True / repeated-backward pattern do_bench
# uses elsewhere. Run a fresh fwd+bwd each iteration so each backward sees an
# unmodified autograd graph. Measurement therefore includes forward time —
# subtract the "forward" measurement to derive backward-only timing.
def _fwd_bwd():
if logits.grad is not None:
logits.grad = None
out = fwd_fn(logits, target)
out.sum().backward()
ms_50, ms_20, ms_80 = triton.testing.do_bench(_fwd_bwd, rep=100, quantiles=QUANTILES)
elif mode == "full":
def full():
y = fwd()
y.sum().backward()
ms_50, ms_20, ms_80 = triton.testing.do_bench(full, rep=100, quantiles=QUANTILES)
else:
raise ValueError(f"unknown mode: {mode!r}")
return SingleBenchmarkRunOutput(y_20=ms_20, y_50=ms_50, y_80=ms_80)
def bench_memory_megatron_cross_entropy(input: SingleBenchmarkRunInput) -> SingleBenchmarkRunOutput:
v = input.x
provider = input.kernel_provider
s = input.extra_benchmark_config["S"]
b = input.extra_benchmark_config["B"]
logits, target = _make_inputs(s, b, v)
fwd_fn = _select_fwd(provider)
def full():
y = fwd_fn(logits, target)
y.sum().backward()
mem_50, mem_20, mem_80 = _test_memory(full, quantiles=QUANTILES)
return SingleBenchmarkRunOutput(y_20=mem_20, y_50=mem_50, y_80=mem_80)
if __name__ == "__main__":
args = parse_benchmark_script_args()
providers = ["liger", "torch"]
if _MEGATRON_AVAILABLE:
providers.append("megatron")
providers.append("megatron-unfused")
common_configs = {
"kernel_name": "megatron_cross_entropy",
"x_name": "V",
"x_label": "vocab size",
"x_values": [2**i for i in range(12, 18)],
"kernel_providers": providers,
"extra_benchmark_configs": [{"S": 2048, "B": 4}],
"overwrite": args.overwrite,
}
run_benchmarks(
bench_test_fn=bench_speed_megatron_cross_entropy,
kernel_operation_modes=["forward", "backward", "full"],
metric_name="speed",
metric_unit="ms",
**common_configs,
)
run_benchmarks(
bench_test_fn=bench_memory_megatron_cross_entropy,
kernel_operation_modes=["full"],
metric_name="memory",
metric_unit="MB",
**common_configs,
)
+44
View File
@@ -91,3 +91,47 @@ You can also use the Patching APIs to use the kernels for a specific model archi
extra:
show_docstring: true
show_signature: true
---
## Megatron-LM
Liger also exposes a patch for the [Megatron-LM](https://github.com/NVIDIA/Megatron-LM)
training framework, replacing Megatron's native RMSNorm and both vocab-parallel
cross-entropy paths (fused and unfused) with Liger's Triton kernels.
| **Framework** | **API** | **Supported Operations** |
|---------------|--------------------------------------------------------|--------------------------|
| Megatron-LM | `liger_kernel.megatron.apply_liger_kernel_to_megatron` | RMSNorm, CrossEntropyLoss |
**Scope**: Initial release supports `tensor_model_parallel_size=1` only for
cross-entropy. Vocab-parallel cross-entropy (TP>1) is follow-up work — with
TP>1, each rank holds a sharded `[N, V/tp]` logits slice and cross-entropy
requires cross-rank all-reduces that Liger's kernel does not perform. The
patch raises a `RuntimeError` at patch time or call time if TP>1 is detected.
**Usage**:
```python
from liger_kernel.megatron import apply_liger_kernel_to_megatron
# Call before Megatron's forward pass reaches compute_language_model_loss.
# Defaults match Megatron's native CE behavior; no CE-specific config needed.
apply_liger_kernel_to_megatron(rms_norm=True, cross_entropy=True)
```
Both the fused (`config.cross_entropy_loss_fusion=True`,
`cross_entropy_fusion_impl='native'`) and unfused
(`config.cross_entropy_loss_fusion=False`) CE paths are patched in a single
call, so Megatron picks up Liger regardless of which path your config selects.
For training setups that need explicit kernel configuration (custom
`ignore_index`, `label_smoothing`, etc.), instantiate
`LigerMegatronCrossEntropy` directly and wire it into your model — see
`examples/megatron/run_mode2_hand_spec.py`.
::: liger_kernel.megatron.apply_liger_kernel_to_megatron
options:
extra:
show_docstring: true
show_signature: true
+29 -11
View File
@@ -1,20 +1,24 @@
"""Mode 1 — monkey-patch Megatron-Core to use Liger RMSNorm.
"""Mode 1 — monkey-patch Megatron-Core to use Liger RMSNorm + cross-entropy.
Adapted from Megatron's ``examples/run_simple_mcore_train_loop.py``. The
relevant additions (vs. that file) are:
1. ``apply_liger_kernel_to_megatron(rms_norm=True)`` called once at the
top of ``model_provider()``. This patches both
``LocalSpecProvider.layer_norm`` (per-layer norm slots) and
``transformer_block.LayerNormImpl`` (block-level ``final_layernorm``).
1. ``apply_liger_kernel_to_megatron(rms_norm=True, cross_entropy=True)``
called once at the top of ``model_provider()``. This patches:
- ``LocalSpecProvider.layer_norm`` (per-layer norm slots)
- ``transformer_block.LayerNormImpl`` (block-level ``final_layernorm``)
- ``fused_cross_entropy.fused_vocab_parallel_cross_entropy``
(the fused CE path)
- ``tensor_parallel.cross_entropy.vocab_parallel_cross_entropy``
(the unfused CE path)
2. ``normalization="RMSNorm"`` added to ``TransformerConfig`` so the
model actually has RMSNorm slots to patch (Megatron defaults to
``LayerNorm``).
3. ``_print_norm_classes`` after model construction — prints the
resolved class for every norm slot so you can verify Liger took
over.
3. ``_print_norm_classes`` + ``_print_ce_symbols`` after model construction
— print the resolved class/function bindings so you can verify Liger
took over for every slot.
Run with:
torchrun --nproc_per_node=2 --master_addr=127.0.0.1 --master_port=29500 \\
@@ -70,7 +74,7 @@ def initialize_distributed(tp: int = 2, pp: int = 1) -> None:
def model_provider() -> GPTModel:
# ↓↓ Mode 1 — patch once, everything below picks up Liger ↓↓
apply_liger_kernel_to_megatron(rms_norm=True)
apply_liger_kernel_to_megatron(rms_norm=True, cross_entropy=True)
# ↑↑ ------------------------------------------------------ ↑↑
cfg = TransformerConfig(
@@ -84,7 +88,7 @@ def model_provider() -> GPTModel:
return GPTModel(
config=cfg,
transformer_layer_spec=get_gpt_layer_local_spec(normalization="RMSNorm"),
vocab_size=100,
vocab_size=128,
max_sequence_length=_SEQUENCE_LENGTH,
)
@@ -142,8 +146,21 @@ def _print_norm_classes(model: torch.nn.Module) -> None:
print()
def _print_ce_symbols() -> None:
"""Show the current bindings of Megatron's two CE entry points."""
import megatron.core.fusions.fused_cross_entropy as fused
import megatron.core.tensor_parallel.cross_entropy as unfused
print("\n=== Resolved CE symbols ===")
print(f" fused.fused_vocab_parallel_cross_entropy → {fused.fused_vocab_parallel_cross_entropy.__name__}")
print(f" unfused.vocab_parallel_cross_entropy → {unfused.vocab_parallel_cross_entropy.__name__}")
print()
def main() -> None:
initialize_distributed(tp=2, pp=1)
# TP=1, DP=2 — CE patch (TP=1 only). Norms are correct under any TP value, so
# demonstrating both Liger features in one script means running data-parallel.
initialize_distributed(tp=1, pp=1)
model_parallel_cuda_manual_seed(123)
torch.manual_seed(123)
@@ -153,6 +170,7 @@ def main() -> None:
print("\n=== Full model tree (mode 1: monkey-patch) ===")
print(gpt_model)
_print_norm_classes(gpt_model)
_print_ce_symbols()
ddp_cfg = DistributedDataParallelConfig(
grad_reduce_in_fp32=False,
+62 -13
View File
@@ -1,22 +1,28 @@
"""Mode 2 — hand-assembled TransformerBlockSubmodules using LigerMegatronRMSNorm.
"""Mode 2 — hand-assembled spec + GPTModel subclass using Liger directly.
Adapted from Megatron's ``examples/run_simple_mcore_train_loop.py``. The
relevant additions (vs. that file) are:
1. Direct import of ``LigerMegatronRMSNorm`` (no monkey-patch).
1. Direct imports of ``LigerMegatronRMSNorm`` and ``LigerMegatronCrossEntropy``
(no monkey-patch).
2. ``model_provider()`` assembles a ``TransformerBlockSubmodules`` by
hand, placing ``LigerMegatronRMSNorm`` into every norm slot:
2. ``model_provider()`` assembles a ``TransformerBlockSubmodules`` by hand,
placing ``LigerMegatronRMSNorm`` into every norm slot:
- per-layer ``input_layernorm`` and ``pre_mlp_layernorm``
- the block-level ``layer_norm`` field that backs
``decoder.final_layernorm``
This is the slot-level integration path — verbose but maximally
explicit. It is the only way to control ``final_layernorm`` from
user code without monkey-patching.
This is the slot-level integration path — verbose but maximally explicit.
It is the only way to control ``final_layernorm`` from user code without
monkey-patching.
3. ``_print_norm_classes`` after model construction — prints the
resolved class for every norm slot so you can verify Liger took
over.
3. ``_LigerCEGPTModel(GPTModel)`` overrides
``LanguageModule.compute_language_model_loss`` to route the loss through
a ``LigerMegatronCrossEntropy`` instance. Cross-entropy has no spec slot
in Megatron, so subclassing is the symmetric "hand-built" path.
4. ``_print_norm_classes`` + ``_print_ce_class`` after model construction
— print the resolved class for every norm slot AND the resolved CE
class on the model so you can verify Liger took over.
Run with:
torchrun --nproc_per_node=2 --master_addr=127.0.0.1 --master_port=29500 \\
@@ -65,6 +71,7 @@ from torch.optim import Adam
from torch.utils.data import DataLoader
# --- Liger integration: Mode 2 ---------------------------------------------
from liger_kernel.megatron import LigerMegatronCrossEntropy
from liger_kernel.megatron import LigerMegatronRMSNorm
# ---------------------------------------------------------------------------
@@ -72,6 +79,31 @@ from liger_kernel.megatron import LigerMegatronRMSNorm
_SEQUENCE_LENGTH = 64
_NUM_ITERS = 5
_NUM_LAYERS = 2
_LABEL_SMOOTHING = 0.1
class _LigerCEGPTModel(GPTModel):
"""``GPTModel`` subclass that routes its loss through ``LigerMegatronCrossEntropy``.
Megatron's CE is not a spec slot — ``LanguageModule.compute_language_model_loss``
calls ``fused_vocab_parallel_cross_entropy`` directly. The symmetric "hand-built"
integration is therefore to subclass ``GPTModel`` and override that method.
"""
def __init__(self, *args, liger_ce_label_smoothing: float = 0.0, **kwargs):
super().__init__(*args, **kwargs)
self.liger_ce = LigerMegatronCrossEntropy(
ignore_index=-100,
label_smoothing=liger_ce_label_smoothing,
reduction="none",
)
def compute_language_model_loss(self, labels, logits):
# LanguageModule contract: input labels are [b, s], output loss is [b, s].
# LigerMegatronCrossEntropy matches the fused signature, which expects [s, b].
labels_sb = labels.transpose(0, 1).contiguous() # [s, b]
loss_sb = self.liger_ce(logits, labels_sb, self.pg_collection.tp) # [s, b]
return loss_sb.transpose(0, 1).contiguous() # [b, s]
def initialize_distributed(tp: int = 2, pp: int = 1) -> None:
@@ -133,11 +165,12 @@ def model_provider() -> GPTModel:
)
# ↑↑ ----------------------------------------------------------------- ↑↑
return GPTModel(
return _LigerCEGPTModel(
config=cfg,
transformer_layer_spec=block_spec,
vocab_size=100,
vocab_size=128,
max_sequence_length=_SEQUENCE_LENGTH,
liger_ce_label_smoothing=_LABEL_SMOOTHING,
)
@@ -194,8 +227,23 @@ def _print_norm_classes(model: torch.nn.Module) -> None:
print()
def _print_ce_class(model: torch.nn.Module) -> None:
"""Show that ``compute_language_model_loss`` will route through Liger."""
ce = getattr(model, "liger_ce", None)
print("=== Resolved CE class ===")
if ce is None:
print(" model.liger_ce → (not set; subclass missing)")
else:
print(f" model.liger_ce → {type(ce).__module__}.{type(ce).__name__}")
print(f" ce.label_smoothing → {ce.label_smoothing}")
print(f" ce.ignore_index → {ce.ignore_index}")
print()
def main() -> None:
initialize_distributed(tp=2, pp=1)
# TP=1, DP=2 — CE patch (TP=1 only). Norms are correct under any TP value, so
# demonstrating both Liger features in one script means running data-parallel.
initialize_distributed(tp=1, pp=1)
model_parallel_cuda_manual_seed(123)
torch.manual_seed(123)
@@ -205,6 +253,7 @@ def main() -> None:
print("\n=== Full model tree (mode 2: hand-built spec) ===")
print(gpt_model)
_print_norm_classes(gpt_model)
_print_ce_class(gpt_model)
ddp_cfg = DistributedDataParallelConfig(
grad_reduce_in_fp32=False,
+12 -3
View File
@@ -3,11 +3,20 @@
Public API:
LigerMegatronRMSNorm — RMSNorm module conforming to Megatron-Core's
LayerNormBuilder protocol.
apply_liger_kernel_to_megatron — patches Megatron-Core's BackendSpecProvider
so existing scripts pick up Liger kernels with one line.
LigerMegatronCrossEntropy — nn.Module drop-in for Megatron's vocab-parallel
cross-entropy (fused signature).
apply_liger_kernel_to_megatron — patches Megatron-Core so existing training
scripts pick up Liger kernels with one line. Currently supports
RMSNorm (via BackendSpecProvider) plus both the fused and unfused
vocab-parallel cross-entropy paths.
"""
from liger_kernel.megatron.cross_entropy import LigerMegatronCrossEntropy
from liger_kernel.megatron.monkey_patch import apply_liger_kernel_to_megatron
from liger_kernel.megatron.rms_norm import LigerMegatronRMSNorm
__all__ = ["LigerMegatronRMSNorm", "apply_liger_kernel_to_megatron"]
__all__ = [
"LigerMegatronCrossEntropy",
"LigerMegatronRMSNorm",
"apply_liger_kernel_to_megatron",
]
@@ -0,0 +1,80 @@
"""Megatron-Core compatible cross-entropy backed by Liger's Triton kernel."""
from __future__ import annotations
import torch
from liger_kernel.transformers.functional import liger_cross_entropy
class LigerMegatronCrossEntropy(torch.nn.Module):
"""``nn.Module`` drop-in for Megatron's vocab-parallel cross-entropy.
Conforms to ``megatron.core.fusions.fused_cross_entropy.fused_vocab_parallel_cross_entropy``'s
signature, ``(vocab_parallel_logits, target, tp_group=None)``. Public Mode-2 (hand-built)
API: instantiate once with the per-training-run config, then call from your overridden
``LanguageModule.compute_language_model_loss`` (or wherever Megatron's CE would live in your
custom model).
Mirrors the ``LigerMegatronRMSNorm`` pattern shipped in PR #1254: config-time kwargs on
``__init__``, data-only ``forward``. Single source of truth for the underlying Liger call;
the monkey-patch wrappers in ``monkey_patch.py`` instantiate this class.
Args:
ignore_index: Target index to ignore.
label_smoothing: Cross-entropy label smoothing factor.
reduction: Must be ``"none"`` — Megatron's vocab-parallel CE contract returns per-token
loss shaped ``[seq, batch]`` and handles reduction itself downstream.
Scope:
TP=1 only. Vocab-parallel cross-entropy (TP>1) requires cross-rank reductions
that Liger's kernel does not perform; tracked as Phase 1.5 follow-up. Raises
``RuntimeError`` at call time if a multi-rank ``tp_group`` is supplied.
"""
def __init__(
self,
ignore_index: int = -100,
label_smoothing: float = 0.0,
reduction: str = "none",
):
super().__init__()
if reduction != "none":
raise ValueError(
f"Megatron's vocab-parallel CE contract requires per-token loss; "
f"reduction must be 'none', got {reduction!r}."
)
self.ignore_index = ignore_index
self.label_smoothing = label_smoothing
self.reduction = reduction
def forward(
self,
vocab_parallel_logits: torch.Tensor,
target: torch.Tensor,
tp_group=None,
) -> torch.Tensor:
if tp_group is not None and hasattr(tp_group, "size") and tp_group.size() > 1:
raise RuntimeError(
f"LigerMegatronCrossEntropy requires tensor_model_parallel_size=1, "
f"got tp_group.size()={tp_group.size()}. Vocab-parallel support is "
f"tracked as follow-up work."
)
if vocab_parallel_logits.dim() != 3:
raise ValueError(
f"vocab_parallel_logits must be 3-D ([seq, batch, vocab]); "
f"got shape {tuple(vocab_parallel_logits.shape)}. (HuggingFace's "
f"[batch, seq, vocab] callers must transpose before calling.)"
)
s, b, v = vocab_parallel_logits.shape
loss = liger_cross_entropy(
vocab_parallel_logits.reshape(-1, v),
target.reshape(-1),
ignore_index=self.ignore_index,
label_smoothing=self.label_smoothing,
reduction=self.reduction,
)
return loss.reshape(s, b)
def extra_repr(self) -> str:
return f"ignore_index={self.ignore_index}, label_smoothing={self.label_smoothing}, reduction={self.reduction!r}"
+177 -9
View File
@@ -9,12 +9,16 @@ logger = logging.getLogger(__name__)
_PATCH_MARKER = "__liger_patched__"
def apply_liger_kernel_to_megatron(rms_norm: bool = True) -> None:
def apply_liger_kernel_to_megatron(
rms_norm: bool = True,
cross_entropy: bool = False,
) -> None:
"""Patch Megatron-Core to use Liger Triton kernels.
Idempotent. Targets Megatron's ``BackendSpecProvider`` and
``transformer_block.LayerNormImpl`` so every model that routes through
the standard spec system benefits without per-model code.
Idempotent. Targets Megatron's ``BackendSpecProvider``,
``transformer_block.LayerNormImpl``, and (optionally) both of Megatron's
vocab-parallel cross-entropy entry points so models that route through
the standard spec system pick up Liger without per-model code.
Args:
rms_norm: When ``True`` (default) replace both
@@ -22,20 +26,69 @@ def apply_liger_kernel_to_megatron(rms_norm: bool = True) -> None:
``transformer_block.LayerNormImpl`` (the block-level
``final_layernorm`` slot) so all RMSNorm modules in the model
become ``LigerMegatronRMSNorm``.
cross_entropy: When ``True`` replace both
``megatron.core.fusions.fused_cross_entropy.fused_vocab_parallel_cross_entropy``
(fused path) and
``megatron.core.tensor_parallel.cross_entropy.vocab_parallel_cross_entropy``
(unfused path) with Liger's Triton cross-entropy. Default
``False`` because this path currently supports
``tensor_model_parallel_size=1`` only. The fused wrapper matches
native's ``(logits, target, tp_group)`` signature exactly; the
unfused wrapper additionally honors a runtime ``label_smoothing``
argument, matching native's
``(logits, target, label_smoothing=0.0, tp_group=None)``.
Notes:
Call this BEFORE building your model. Patching after instantiation
will not retroactively swap modules already created.
This only affects the local (non-TE) backend. Mixing Liger norms with
``TESpecProvider`` requires a custom ``BackendSpecProvider`` subclass
because TE's ``TELayerNormColumnParallelLinear`` folds the norm into
the QKV linear; naive substitution would either double-norm or skip
the norm. See the project README for the mixing recipe.
The RMSNorm patches only affect the local (non-TE) backend. Mixing
Liger norms with ``TESpecProvider`` requires a custom
``BackendSpecProvider`` subclass because TE's
``TELayerNormColumnParallelLinear`` folds the norm into the QKV
linear; naive substitution would either double-norm or skip the norm.
For explicit kernel configuration (custom ``ignore_index``,
``label_smoothing``, etc.) instantiate ``LigerMegatronCrossEntropy``
directly and wire it into your model (Mode 2). The monkey-patch path
is intentionally a transparent drop-in: it matches Megatron's native
defaults so callers can flip Liger on without touching loss config.
Raises:
RuntimeError: When ``cross_entropy=True`` and Megatron's parallel
state already reports ``tensor_model_parallel_size > 1``.
"""
if rms_norm:
_patch_local_spec_provider_layer_norm()
_patch_transformer_block_layernorm_impl()
if cross_entropy:
_check_tensor_parallel_size_at_patch_time()
_patch_fused_vocab_parallel_cross_entropy()
_patch_vocab_parallel_cross_entropy()
def _check_tensor_parallel_size_at_patch_time() -> None:
"""Raise RuntimeError if Megatron's parallel state already reports TP>1.
If Megatron is importable but the parallel state is not yet initialized
(for example, ``apply_liger_kernel_to_megatron`` is called before
``initialize_megatron``), silently defer; per-kernel wrappers check again
at call time against the ``tp_group`` argument Megatron supplies.
"""
try:
from megatron.core import parallel_state
except ImportError:
return
try:
tp_size = parallel_state.get_tensor_model_parallel_world_size()
except (AssertionError, RuntimeError):
return
if tp_size > 1:
raise RuntimeError(
f"apply_liger_kernel_to_megatron(cross_entropy=True) currently requires "
f"tensor_model_parallel_size=1, got {tp_size}. Vocab-parallel cross-entropy "
f"support is planned as follow-up work."
)
def _patch_local_spec_provider_layer_norm() -> None:
@@ -120,3 +173,118 @@ def _patch_transformer_block_layernorm_impl() -> None:
"Patched megatron.core.transformer.transformer_block.LayerNormImpl "
"to route RMSNorm configs through LigerMegatronRMSNorm."
)
# Sentinel for "caller did not pass this kwarg". A plain ``0.0`` default would be
# observationally indistinguishable from "user explicitly asked for 0.0" — and Megatron's
# native vocab_parallel_cross_entropy accepts call-time ``label_smoothing=0.0`` as a real
# request for that value. We must not silently override.
_LABEL_SMOOTHING_UNSET = object()
def _patch_fused_vocab_parallel_cross_entropy() -> None:
"""Replace ``megatron.core.fusions.fused_cross_entropy.fused_vocab_parallel_cross_entropy``.
Wraps a single ``LigerMegatronCrossEntropy`` instance (constructed with class defaults
that match Megatron's native fused-CE behavior) in a closure matching Megatron's fused-CE
signature ``(logits, target, tp_group)``. Idempotent: a sentinel attribute on the
replacement prevents wrappers from stacking.
"""
try:
import megatron.core.fusions.fused_cross_entropy as fused_ce
except ImportError as exc:
raise ImportError(
"apply_liger_kernel_to_megatron(cross_entropy=True) requires megatron-core to be "
"installed. Expected symbol path: "
"megatron.core.fusions.fused_cross_entropy.fused_vocab_parallel_cross_entropy."
) from exc
if not hasattr(fused_ce, "fused_vocab_parallel_cross_entropy"):
raise ImportError(
"megatron.core.fusions.fused_cross_entropy.fused_vocab_parallel_cross_entropy not "
"found. The symbol path may have changed in your Megatron-LM version. Please file "
"an issue on https://github.com/linkedin/Liger-Kernel with your megatron-core version."
)
if getattr(fused_ce.fused_vocab_parallel_cross_entropy, _PATCH_MARKER, False):
return # already patched
original = fused_ce.fused_vocab_parallel_cross_entropy
from liger_kernel.megatron.cross_entropy import LigerMegatronCrossEntropy
ce = LigerMegatronCrossEntropy()
def liger_fused_vocab_parallel_cross_entropy(vocab_parallel_logits, target, tp_group=None):
return ce(vocab_parallel_logits, target, tp_group=tp_group)
setattr(liger_fused_vocab_parallel_cross_entropy, _PATCH_MARKER, True)
setattr(liger_fused_vocab_parallel_cross_entropy, "__wrapped__", original)
fused_ce.fused_vocab_parallel_cross_entropy = liger_fused_vocab_parallel_cross_entropy
logger.info(
"Patched megatron.core.fusions.fused_cross_entropy.fused_vocab_parallel_cross_entropy with Liger cross-entropy."
)
def _patch_vocab_parallel_cross_entropy() -> None:
"""Replace ``megatron.core.tensor_parallel.cross_entropy.vocab_parallel_cross_entropy``.
This is Megatron's *unfused* eager-Python vocab-parallel CE path, dispatched to when
``config.cross_entropy_loss_fusion=False``. Its signature accepts ``label_smoothing``
at call time, so the wrapper honors a runtime value when the caller actually passed
one. A sentinel disambiguates "caller passed 0.0" (use 0.0) from "caller didn't pass"
(use class default).
"""
try:
import megatron.core.tensor_parallel.cross_entropy as unfused_ce
except ImportError as exc:
raise ImportError(
"apply_liger_kernel_to_megatron(cross_entropy=True) requires megatron-core to be "
"installed. Expected symbol path: "
"megatron.core.tensor_parallel.cross_entropy.vocab_parallel_cross_entropy."
) from exc
if not hasattr(unfused_ce, "vocab_parallel_cross_entropy"):
raise ImportError(
"megatron.core.tensor_parallel.cross_entropy.vocab_parallel_cross_entropy not "
"found. The symbol path may have changed in your Megatron-LM version. Please file "
"an issue on https://github.com/linkedin/Liger-Kernel with your megatron-core version."
)
if getattr(unfused_ce.vocab_parallel_cross_entropy, _PATCH_MARKER, False):
return # already patched
original = unfused_ce.vocab_parallel_cross_entropy
from liger_kernel.megatron.cross_entropy import LigerMegatronCrossEntropy
# Class-default instance; reused for every call where the caller doesn't pass
# label_smoothing. Avoids allocating a fresh module per CE call in the common case
# (Megatron's own LanguageModule.compute_language_model_loss dispatch does not pass
# label_smoothing — it always lands here).
default_ce = LigerMegatronCrossEntropy()
def liger_vocab_parallel_cross_entropy(
vocab_parallel_logits,
target,
label_smoothing=_LABEL_SMOOTHING_UNSET,
tp_group=None,
):
# Sentinel-based "did the caller pass this?" check so that an explicit
# label_smoothing=0.0 from the caller is honored verbatim (matching Megatron's
# native vocab_parallel_cross_entropy contract). Construct a fresh
# LigerMegatronCrossEntropy only on the runtime-override path; nn.Module
# construction is microseconds vs. CE-kernel milliseconds.
if label_smoothing is _LABEL_SMOOTHING_UNSET:
return default_ce(vocab_parallel_logits, target, tp_group=tp_group)
ce = LigerMegatronCrossEntropy(label_smoothing=label_smoothing)
return ce(vocab_parallel_logits, target, tp_group=tp_group)
setattr(liger_vocab_parallel_cross_entropy, _PATCH_MARKER, True)
setattr(liger_vocab_parallel_cross_entropy, "__wrapped__", original)
unfused_ce.vocab_parallel_cross_entropy = liger_vocab_parallel_cross_entropy
logger.info(
"Patched megatron.core.tensor_parallel.cross_entropy.vocab_parallel_cross_entropy with Liger cross-entropy."
)
+489
View File
@@ -0,0 +1,489 @@
"""Correctness tests for ``LigerMegatronCrossEntropy``.
The class is the public Mode-2 API; the monkey-patch wrappers in
``monkey_patch.py`` are thin closures around an instance of this class. Tests
target the class directly — that's the single source of truth for the CE math.
Mirrors ``test/megatron/test_rms_norm.py``'s parametrize style for the
fp32/bf16 sweep so the visual symmetry across the two megatron-side files is
preserved.
"""
import pytest
import torch
import torch.nn.functional as F
from liger_kernel.megatron import LigerMegatronCrossEntropy
from liger_kernel.utils import infer_device
from test.utils import assert_verbose_allclose
from test.utils import set_seed
from test.utils import supports_bfloat16
device = infer_device()
set_seed(42)
def _reference_loss(
vocab_parallel_logits: torch.Tensor,
target: torch.Tensor,
ignore_index: int,
label_smoothing: float,
) -> torch.Tensor:
s, b, v = vocab_parallel_logits.shape
loss_flat = F.cross_entropy(
vocab_parallel_logits.reshape(-1, v).float(),
target.reshape(-1),
reduction="none",
ignore_index=ignore_index,
label_smoothing=label_smoothing,
)
return loss_flat.reshape(s, b)
# ---------------------------------------------------------------------------
# Forward correctness vs. F.cross_entropy reference.
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"s, b, v",
[
(8, 2, 128),
(16, 4, 4096),
(32, 1, 32000),
],
)
@pytest.mark.parametrize(
"dtype, atol, rtol",
[
(torch.float32, 1e-7, 1e-6),
pytest.param(
torch.bfloat16,
1e-2,
1e-2,
marks=pytest.mark.skipif(not supports_bfloat16(), reason="bfloat16 not supported"),
),
],
)
def test_class_matches_pytorch_cross_entropy(s, b, v, dtype, atol, rtol):
"""Headline correctness — forward AND backward parity vs. PyTorch's F.cross_entropy.
Liger writes the gradient back into the input tensor in-place during forward; both paths
are fed independent clones of the same starting tensor so the in-place write on the Liger
side can't corrupt the reference path."""
ce = LigerMegatronCrossEntropy()
base = torch.randn(s, b, v, device=device, dtype=dtype) * 0.5
target = torch.randint(0, v, (s, b), device=device, dtype=torch.long)
h_ref = base.detach().clone().requires_grad_(True)
h_got = base.detach().clone().requires_grad_(True)
ref = _reference_loss(h_ref, target, ignore_index=-100, label_smoothing=0.0)
got = ce(h_got, target)
assert got.shape == (s, b)
assert_verbose_allclose(got.float(), ref.float(), atol=atol, rtol=rtol)
ref.sum().backward()
got.sum().backward()
assert_verbose_allclose(h_got.grad.float(), h_ref.grad.float(), atol=atol, rtol=rtol)
# ---------------------------------------------------------------------------
# Configuration plumbing — wrapper-specific contracts.
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("ignore_index", [-100, 0])
def test_class_respects_ignore_index(ignore_index):
"""ignore_index plumbing — forward AND backward parity. Ignored positions must
contribute zero loss AND zero gradient on the Liger side."""
s, b, v = 16, 2, 1024
ce = LigerMegatronCrossEntropy(ignore_index=ignore_index)
base = torch.randn(s, b, v, device=device, dtype=torch.float32)
target = torch.randint(0, v, (s, b), device=device, dtype=torch.long)
target.view(-1)[: (s * b) // 4] = ignore_index
h_ref = base.detach().clone().requires_grad_(True)
h_got = base.detach().clone().requires_grad_(True)
ref = _reference_loss(h_ref, target, ignore_index=ignore_index, label_smoothing=0.0)
got = ce(h_got, target)
assert_verbose_allclose(got.float(), ref.float(), atol=1e-6, rtol=1e-5)
ref.sum().backward()
got.sum().backward()
assert_verbose_allclose(h_got.grad.float(), h_ref.grad.float(), atol=1e-6, rtol=1e-5)
@pytest.mark.parametrize("label_smoothing", [0.0, 0.1])
def test_class_respects_label_smoothing(label_smoothing):
"""label_smoothing plumbing — forward AND backward parity. Liger and PyTorch share the
same smoothing formula but with different intermediate kernels; gradient check guards
against algebraic-equivalence-but-numerical-divergence bugs."""
s, b, v = 8, 2, 512
ce = LigerMegatronCrossEntropy(label_smoothing=label_smoothing)
base = torch.randn(s, b, v, device=device, dtype=torch.float32)
target = torch.randint(0, v, (s, b), device=device, dtype=torch.long)
h_ref = base.detach().clone().requires_grad_(True)
h_got = base.detach().clone().requires_grad_(True)
ref = _reference_loss(h_ref, target, ignore_index=-100, label_smoothing=label_smoothing)
got = ce(h_got, target)
assert_verbose_allclose(got.float(), ref.float(), atol=1e-5, rtol=1e-4)
ref.sum().backward()
got.sum().backward()
assert_verbose_allclose(h_got.grad.float(), h_ref.grad.float(), atol=1e-5, rtol=1e-4)
@pytest.mark.parametrize("bad_reduction", ["mean", "sum", "MEAN", "garbage"])
def test_class_rejects_non_none_reduction(bad_reduction):
"""Megatron's contract is per-token loss; mean/sum break the [s, b] return shape."""
with pytest.raises(ValueError, match="reduction must be 'none'"):
LigerMegatronCrossEntropy(reduction=bad_reduction)
def test_class_rejects_non_3d_logits():
"""The class explicitly guards against HuggingFace-shape [b, s, v] callers etc."""
ce = LigerMegatronCrossEntropy()
bad = torch.randn(8, 16, device=device) # 2-D
target = torch.randint(0, 16, (8,), device=device, dtype=torch.long)
with pytest.raises(ValueError, match="3-D"):
ce(bad, target)
too_many = torch.randn(2, 2, 4, 16, device=device) # 4-D
target2 = torch.randint(0, 16, (2, 2, 4), device=device, dtype=torch.long)
with pytest.raises(ValueError, match="3-D"):
ce(too_many, target2)
# ---------------------------------------------------------------------------
# TP guard — the only safety net the class itself enforces.
# ---------------------------------------------------------------------------
def test_class_raises_on_tp_group_size_greater_than_one():
ce = LigerMegatronCrossEntropy()
logits = torch.randn(4, 1, 32, device=device)
target = torch.randint(0, 32, (4, 1), device=device, dtype=torch.long)
class _FakeGroup:
def size(self):
return 2
with pytest.raises(RuntimeError, match="tensor_model_parallel_size=1"):
ce(logits, target, tp_group=_FakeGroup())
def test_class_accepts_single_rank_tp_group():
ce = LigerMegatronCrossEntropy()
logits = torch.randn(4, 1, 32, device=device)
target = torch.randint(0, 32, (4, 1), device=device, dtype=torch.long)
class _FakeGroup:
def size(self):
return 1
out = ce(logits, target, tp_group=_FakeGroup())
assert out.shape == (4, 1)
# ---------------------------------------------------------------------------
# Gradient sanity — Liger's CE writes gradients in place; verify the class
# preserves them through Megatron's [s, b, v] reshape contract.
# ---------------------------------------------------------------------------
def test_class_preserves_gradients():
"""Backward smoke test — grad exists with correct shape AND matches PyTorch's reference.
Previously asserted only ``grad is not None`` + shape; that gave a misleadingly green test
when grad values were wrong. Now compares against ``F.cross_entropy(...).sum().backward()``."""
s, b, v = 8, 2, 256
ce = LigerMegatronCrossEntropy()
base = torch.randn(s, b, v, device=device, dtype=torch.float32)
target = torch.randint(0, v, (s, b), device=device, dtype=torch.long)
h_ref = base.detach().clone().requires_grad_(True)
h_got = base.detach().clone().requires_grad_(True)
ref = _reference_loss(h_ref, target, ignore_index=-100, label_smoothing=0.0)
got = ce(h_got, target)
ref.sum().backward()
got.sum().backward()
assert h_got.grad is not None
assert h_got.grad.shape == h_got.shape
assert_verbose_allclose(h_got.grad.float(), h_ref.grad.float(), atol=1e-6, rtol=1e-5)
def test_class_extra_repr():
ce = LigerMegatronCrossEntropy(ignore_index=42, label_smoothing=0.07)
rep = ce.extra_repr()
assert "ignore_index=42" in rep
assert "label_smoothing=0.07" in rep
assert "reduction='none'" in rep
# ---------------------------------------------------------------------------
# Beefier sweeps adapted from test/transformers/test_cross_entropy.py.
#
# LigerMegatronCrossEntropy is a [s, b, v] -> [s, b] reshape around Liger's
# CE op (reduction='none'). Numerical behavior should match the kernel itself,
# so the same parametrization patterns the kernel suite uses are the right
# coverage shape here — just adapted to the 3-D contract.
# ---------------------------------------------------------------------------
def _assign_ignore_index(target: torch.Tensor, ignore_index: int, frac: float = 0.25) -> None:
"""In-place: replace ~frac of target positions with ignore_index.
Matches the transformers-side helpers that randomize the masked-out indices
so the test isn't degenerate on a particular row layout.
"""
flat = target.view(-1)
n = max(1, int(flat.numel() * frac))
idx = torch.randperm(flat.numel(), device=flat.device)[:n]
flat[idx] = ignore_index
@pytest.mark.parametrize(
"s, b, v",
[
(16, 1, 4096),
(32, 2, 32000), # llama-ish vocab
(5, 3, 123), # weird shape
],
)
@pytest.mark.parametrize("scalar", [0.5, 1.0, 5.0])
@pytest.mark.parametrize(
"dtype, atol, rtol",
[
(torch.float32, 1e-7, 1e-6),
pytest.param(
torch.bfloat16,
1e-2,
5e-2,
marks=pytest.mark.skipif(not supports_bfloat16(), reason="bfloat16 not supported"),
),
],
)
def test_class_correctness_scalar_sweep(s, b, v, scalar, dtype, atol, rtol):
"""Vary input magnitude — guards against numerical drift at large logit scales
(mirrors the ``scalar`` parametrize in ``test/transformers/test_cross_entropy.py``)."""
ce = LigerMegatronCrossEntropy()
base = torch.randn(s, b, v, device=device, dtype=dtype) * scalar
target = torch.randint(0, v, (s, b), device=device, dtype=torch.long)
# Backward parity: feed the same starting tensor through both paths.
h_ref = base.detach().clone().requires_grad_(True)
h_got = base.detach().clone().requires_grad_(True)
ref = _reference_loss(h_ref, target, ignore_index=-100, label_smoothing=0.0)
got = ce(h_got, target)
assert got.shape == (s, b)
assert_verbose_allclose(got.float(), ref.float(), atol=atol, rtol=rtol)
ref.sum().backward()
got.sum().backward()
assert_verbose_allclose(h_got.grad.float(), h_ref.grad.float(), atol=atol, rtol=rtol)
@pytest.mark.parametrize(
"s, b, v, ignore_index",
[
(16, 1, 4096, -100), # standard hf sentinel
(32, 2, 32000, 2), # positive id (valid vocab slot used as ignore)
(5, 3, 123, -123), # weird negative
],
)
@pytest.mark.parametrize(
"dtype, atol, rtol",
[
(torch.float32, 1e-7, 1e-6),
pytest.param(
torch.bfloat16,
1e-2,
5e-2,
marks=pytest.mark.skipif(not supports_bfloat16(), reason="bfloat16 not supported"),
),
],
)
def test_class_correctness_with_ignore_index_sweep(s, b, v, ignore_index, dtype, atol, rtol):
"""Broader ignore_index sweep including positive/negative sentinels and forward+backward
correctness vs. PyTorch's reference. Mirrors transformers-side ``test_correctness_with_ignore_index``."""
ce = LigerMegatronCrossEntropy(ignore_index=ignore_index)
base = torch.randn(s, b, v, device=device, dtype=dtype)
target = torch.randint(0, v, (s, b), device=device, dtype=torch.long)
_assign_ignore_index(target, ignore_index, frac=0.3)
h_ref = base.detach().clone().requires_grad_(True)
h_got = base.detach().clone().requires_grad_(True)
ref = _reference_loss(h_ref, target, ignore_index=ignore_index, label_smoothing=0.0)
got = ce(h_got, target)
assert_verbose_allclose(got.float(), ref.float(), atol=atol, rtol=rtol)
ref.sum().backward()
got.sum().backward()
assert_verbose_allclose(h_got.grad.float(), h_ref.grad.float(), atol=atol, rtol=rtol)
@pytest.mark.parametrize(
"s, b, v, ignore_index, label_smoothing",
[
(16, 1, 4096, 1, 0.1),
(32, 2, 32000, -100, 0.2),
(5, 3, 123, -300, 0.05),
],
)
@pytest.mark.parametrize(
"dtype, atol, rtol",
[
(torch.float32, 1e-6, 1e-5),
pytest.param(
torch.bfloat16,
1e-2,
5e-2,
marks=pytest.mark.skipif(not supports_bfloat16(), reason="bfloat16 not supported"),
),
],
)
def test_class_correctness_with_label_smoothing_and_ignore_index(
s,
b,
v,
ignore_index,
label_smoothing,
dtype,
atol,
rtol,
):
"""Combined ignore_index × label_smoothing sweep — the two are independent in Liger's CE
kernel but mixing them historically surfaced bugs in the smoothing math. Mirrors
``test_correctness_with_label_smoothing_with_ignore_index_once`` from the kernel suite."""
ce = LigerMegatronCrossEntropy(ignore_index=ignore_index, label_smoothing=label_smoothing)
base = torch.randn(s, b, v, device=device, dtype=dtype)
target = torch.randint(0, v, (s, b), device=device, dtype=torch.long)
_assign_ignore_index(target, ignore_index, frac=0.25)
h_ref = base.detach().clone().requires_grad_(True)
h_got = base.detach().clone().requires_grad_(True)
ref = _reference_loss(h_ref, target, ignore_index=ignore_index, label_smoothing=label_smoothing)
got = ce(h_got, target)
assert_verbose_allclose(got.float(), ref.float(), atol=atol, rtol=rtol)
ref.sum().backward()
got.sum().backward()
assert_verbose_allclose(h_got.grad.float(), h_ref.grad.float(), atol=atol, rtol=rtol)
@pytest.mark.parametrize(
"s, b, v",
[
(16, 1, 4096),
(5, 3, 123),
],
)
@pytest.mark.parametrize(
"dtype, atol, rtol",
[
(torch.float32, 1e-6, 1e-5),
pytest.param(
torch.bfloat16,
1e-2,
5e-2,
marks=pytest.mark.skipif(not supports_bfloat16(), reason="bfloat16 not supported"),
),
],
)
def test_class_correctness_not_last_layer(s, b, v, dtype, atol, rtol):
"""Loss is multiplied by a downstream factor before ``.backward(grad_output)`` — verifies
that Liger's in-place gradient write through the wrapper survives non-trivial chained
autograd (i.e. CE isn't the last op in the graph). Mirrors transformers-side
``test_correctness_not_last_layer``."""
ce = LigerMegatronCrossEntropy()
base = torch.randn(s, b, v, device=device, dtype=dtype)
target = torch.randint(0, v, (s, b), device=device, dtype=torch.long)
h_ref = base.detach().clone().requires_grad_(True)
h_got = base.detach().clone().requires_grad_(True)
ref = _reference_loss(h_ref, target, ignore_index=-100, label_smoothing=0.0)
got = ce(h_got, target)
assert_verbose_allclose(got.float(), ref.float(), atol=atol, rtol=rtol)
# Chain: loss = ref * 3 then backward with arbitrary grad_output.
loss_ref = ref * 3.0
loss_got = got * 3.0
grad_out = torch.rand_like(ref)
loss_ref.backward(gradient=grad_out)
loss_got.backward(gradient=grad_out)
assert_verbose_allclose(h_got.grad.float(), h_ref.grad.float(), atol=atol, rtol=rtol)
@pytest.mark.parametrize("ignore_index", [-100, 2])
def test_class_rejects_out_of_bounds_target(ignore_index):
"""Liger's CE kernel asserts target ∈ [0, V); a stray out-of-bounds target should
raise rather than silently produce garbage. Mirrors transformers-side
``test_correctness_with_out_of_bounds_target_once``."""
s, b, v = 8, 2, 64
ce = LigerMegatronCrossEntropy(ignore_index=ignore_index)
logits = torch.randn(s, b, v, device=device, dtype=torch.float32)
target = torch.randint(0, v, (s, b), device=device, dtype=torch.long)
# Plant a couple of out-of-bounds values; ignore_index is permitted but the
# >=V poisoned slots are not.
flat = target.view(-1)
poison = torch.randperm(flat.numel(), device=flat.device)[:2]
flat[poison] = v + 5 # >= V; the kernel-level assert should fire.
with pytest.raises(AssertionError, match="out of bounds"):
ce(logits, target)
@pytest.mark.parametrize(
"dtype, atol, rtol",
[
(torch.float32, 1e-7, 1e-6),
pytest.param(
torch.bfloat16,
1e-2,
5e-2,
marks=pytest.mark.skipif(not supports_bfloat16(), reason="bfloat16 not supported"),
),
],
)
def test_class_correctness_forward_only(dtype, atol, rtol):
"""Forward-only path (under ``torch.no_grad()``) — verifies the wrapper still returns the
right loss when autograd is disabled, AND that a subsequent ``.backward()`` raises the
expected "does not require grad" error. Mirrors transformers-side ``test_correctness_with_forward_only``."""
s, b, v = 16, 2, 1024
ce = LigerMegatronCrossEntropy()
logits_input = torch.randn(s, b, v, device=device, dtype=dtype)
target = torch.randint(0, v, (s, b), device=device, dtype=torch.long)
with torch.no_grad():
# Clone the input separately for each path because Liger writes gradient
# state in-place; sharing a buffer would corrupt the reference.
ref = _reference_loss(logits_input.clone(), target, ignore_index=-100, label_smoothing=0.0)
got = ce(logits_input.clone(), target)
assert_verbose_allclose(got.float(), ref.float(), atol=atol, rtol=rtol)
# Attempting backward on a forward-only output should raise.
with pytest.raises(RuntimeError, match="does not require grad"):
got.sum().backward()
File diff suppressed because it is too large Load Diff