Conversation
|
I agree that making chunking tunable is useful, but I’m not sure C is the right knob to expose. C is only a proxy used to derive chunk_size and num_chunks. Because of cdiv and next_power_of_2, many different C values end up producing the same geometry. For example, with V=262144, H=5376, BT=8192:
It may be cleaner to expose chunk_size or num_chunks directly, since those are the values that actually determine execution. |
The linkedin#1414 chunk geometry derives chunk_size from a C x BT x H memory budget, so the transient [chunk_size, V] logits slab grows linearly with BT and is not tunable: on memory-tight large-vocab runs it becomes the binding allocation (measurements in linkedin#1439). Expose chunk_size itself instead of the C proxy (many C values collapse to the same geometry through cdiv + next_power_of_2): the LIGER_FLCE_CHUNK_SIZE env var selects chunk_size directly, floored to a power of two and clamped to BT per call, read once at import time. Set below the heuristic's choice it doubles as a BT-invariant bound on the slab (chunk_size x V x itemsize bytes); set above, it trades a larger slab for fewer loop iterations. Unset (default) leaves the geometry identical to main.
|
@hiwuhgds-pixel I left Re-validated on the new variable: the FLCE unit suite passes with it unset, |
|
Hi @jin-five and @hiwuhgds-pixel, thanks for the PR and discussing this issue with long context training. I agree that we need a way to adjust the chunk_size on user demand while also having a default setting. Also, I feel like we should have this parameter as a part of the API instead of exposing it as an environment variable. Also, we need to make sure the default behavior is good, and might want to have different defaults for different GPU types(example. H100 has about 80GB memory whereas B300 has about 280GB). I will discuss this further with my team at Linkedin and decide on the default behavior. I am happy to contribute the change to your PR as a maintainer edit if that's okay with you, or leave the implementation to you if you'd prefer. |
|
Hi @vaibhavjindal, thanks for taking a look! Agreed on both points. Exposing chunk_size as part of the API (rather than an env var) makes sense, and GPU-dependent defaults sound reasonable since the memory headroom differs so much across devices. Yes, please feel free to push the change directly to this PR as a maintainer edit. "Allow edits by maintainers" is enabled on the branch. Happy to help with testing or follow-ups once the default behavior is decided. |
The linkedin#1414 chunk geometry derives chunk_size from a C x BT x H memory budget, so the transient [chunk_size, V] logits slab grows linearly with BT and is not tunable: on memory-tight large-vocab runs it becomes the binding allocation (measurements in linkedin#1439). Expose chunk_size itself instead of the C proxy (many C values collapse to the same geometry through cdiv + next_power_of_2): the LIGER_FLCE_CHUNK_SIZE env var selects chunk_size directly, floored to a power of two and clamped to BT per call, read once at import time. Set below the heuristic's choice it doubles as a BT-invariant bound on the slab (chunk_size x V x itemsize bytes); set above, it trades a larger slab for fewer loop iterations. Unset (default) leaves the geometry identical to main.
92f3249 to
3d7c409
Compare
|
@vaibhavjindal I checked the new release and saw that it includes the After checking the latest release, I rebased this PR onto the recent Re-verified on H100 (torch 2.10.0+cu128, triton 3.6.0):
Given that #1472 already resolves my original OOM and adds I'll wait for the team's decision on the API shape and the default policy, and I'm happy to rework this PR accordingly (API argument instead of the env var, GPU-dependent default, or whatever direction you land on). |
Summary
#1414 widened the FLCE token-chunk memory budget to
C x BT x Hwith a hardcoded_CHUNK_MEM_CONST = 16— a clear latency win, but the budget is linear inBT, so the transient[chunk_size, V]logits slab keeps growing on long sequences (V=262144, H=5376: 1.07 GiB at BT=8k, 2.1 GiB at 16k, 4.3 GiB at 32k), beyond theBT <= 8192envelope measured in #1414.On memory-tight runs that slab is a real binding transient on
maintoday via the existingaccum_dtype=torch.float32path. Gemma-4 31B (V=262144, H=5376), DeepSpeed ZeRO-3, no offload, 8x H100 80 GB:expandable_segments:True);LIGER_FLCE_CHUNK_SIZE=256completes deterministically on the identical config, seed, and data — the ~0.9 GiB smaller slab is enough to move the run off that edge.Tried to allocate 2.00 GiBatlogits_chunk = _input_chunk @ weight.t()); a 256-row chunk clears it.A small chunk is not universally better — the default is 3x faster where the memory exists (table below). The problem is that the slab is the only FLCE transient that grows with
BTwhile being completely non-tunable. This PR exposeschunk_sizedirectly viaLIGER_FLCE_CHUNK_SIZE(rows; floored to a power of two, clamped toBTper call; read once at import time). Unset — the default — leaves the geometry identical tomain. Set below the heuristic's choice it doubles as a BT-invariant bound on the slab (chunk_size x V x itemsizebytes); set above, it trades a larger slab for fewer loop iterations.num_chunksstays derived: a fixed chunk count would keep the slab BT-dependent, which is the original problem.Details
Measured trade-off on H100 80GB (full fwd+bwd, bf16, default accumulation path, today's
main; the 256-row geometry):(peaks are transients over the persistent weights/grads, via
torch.cuda.max_memory_allocated; note the 256-row peak is flat inBT— that is the point of the knob. Larger values, e.g. 1024, interpolate the trade-off.)Testing Done
test/transformers/test_fused_linear_cross_entropy.py: 141 passed with the variable unset,=256, and=100(the last normalizes to 64, exercising the power-of-two floor); unset path verified geometry-identical tomainaccum_dtype=fp32, same seed, 128 steps): default geometry OOMs during step 5 (3 out of 3 attempts);LIGER_FLCE_CHUNK_SIZE=256completes (40.11 s/step, allocated peak 75.48 GiB vs 76.36 GiB default). The nominal headroom is misleading: non-PyTorch usage (CUDA context / NCCL / kernel modules) is ~2.6 GiB, so the observed usable allocation budget is roughly ~77 GiB, and the failing run is already at allocator saturation (a 2.62 GiB transient request with 0.75 GiB reserved-but-unallocated). The 0.88 GiB smaller slab moves the configuration off that edgemake testto ensure correctness — verified by paired same-window runs: this branch and unmodifiedmainproduce identical pass/fail/skip counts and identical failure sets in this environment (failures are environmental: corporate proxy blocks some model/asset downloads)make checkstyleto ensure code style — ruff check clean; ruff format reports the same 13 pre-existing files on unmodifiedmainmake test-convergenceto ensure convergence — no new failures vs unmodifiedmain(shared failures are the same environmental set)Test logs (H100-80GB, CUDA 12.8, PyTorch 2.10, Triton 3.6)
FLCE unit suite —
test/transformers/test_fused_linear_cross_entropy.py, run with the variable unset,LIGER_FLCE_CHUNK_SIZE=256, and=100(identical result each time):Full test suite — paired same-window runs, this branch vs unmodified
main(identical failure-name sets; all failures environmental — corporate proxy blocks some model/asset downloads):make test-convergence— same paired protocol: 8 shared failures on both (falcon_h1 / paligemma, environmental); one borderline-tolerance test (qwen3_moe bf16) flips pass/fail on reruns of both the branch and unmodifiedmain:make checkstyle— ruff check clean; ruff format reports only the same 13 pre-existing files as unmodifiedmain. The changed file itself: