Running MiniMax H3 (FL2VA) on 4x L40S with SGLang: dynamic FP8, ~2x speedup vs 2-GPU BF16, 14GB/card #34079
ft54482
started this conversation in
Show and tell
Replies: 1 comment
|
Update (concurrency): Dynamic batching does NOT merge conditioned requests (FL2VA/Ref2VA with keyframe/reference images) — |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Running MiniMax H3 (FL2VA) on 4x L40S with SGLang: FP8 dynamic quantization, ~2x speedup vs 2-GPU BF16
A working recipe for running MiniMax H3 video generation with 4 GPUs in parallel on a single request (Ulysses sequence parallelism), with dynamic FP8 quantization that cuts VRAM to ~1/4 and speeds up generation ~2x compared to 2-GPU BF16 — at the same output quality as the full-precision model.
Environment
sglang-diffusionmultimodal_gen runtime, 2026-08-04 build (Python 3.10, torch 2.x)FL2VA/transformer/model-*.safetensorsetc.)Launch command
Key points:
--num-gpus 4 --ulysses-degree 4: sequence parallelism — one request is sharded across all 4 GPUs.--quantization fp8: dynamic FP8 quantization (per-row weight scales computed at load). This is what enables the VRAM savings.SGLANG_USE_RUNAI_MODEL_STREAMER=0: required — see gotcha Add SRT json decode example #2 below.Results (768p 16:9, 5s, 20 steps)
Gotchas we hit (and fixes)
1. Pruned FP8 checkpoints (e.g.
rzgar/minimax_h3_fl2va_fp8_e4m3fnon ModelScope) are mathematically broken — verify before usingThe pruned conversion replaces the 2688-dim AdaLN time-embedding with an 8-dim curve table (
adaln_t_table [1025, 8]). We verified the conversion against the original full-precision weights (W_pruned @ table[t]vsW_full @ silu(time_embedder(t))):The generation "succeeds" but the video is a flat/gray still (denoised latent collapses to ~0). Do a math check like the one above before adopting any pruned/curve-format H3 checkpoint.
2. Run:ai Model Streamer casts fp8 tensors → disable it
With
SGLANG_USE_RUNAI_MODEL_STREAMERunset, the streamer loads safetensors with fp8 weights pre-cast to bf16. That silently defeats:Set
SGLANG_USE_RUNAI_MODEL_STREAMER=0.3. ComfyUI-style fp8 checkpoints need explicit dequantization by
weight_scaleComfyUI-style fp8 checkpoints store weights as
fp8_value = weight / weight_scale(per-tensor scalarweight_scale, e.g. 0.0107) plusinput_scale/comfy_quantkeys. SGLang's_maybe_dequantize_fp8looks upweight_scalefrom the mapped state dict — buthf_to_custom_state_dictfilters keys byvalid_target_names=set(model.state_dict().keys()), and a dynamically-quantized model has noweight_scaleparameters → the scale is never found → weights load ~90x too large. We added a module-level scale registry populated from the checkpoint files (see patch below).4. Loading a 62 GB full-precision model with
--quantization fp8OOMs on 4x45 GBThe post-load quantization (
process_weights_after_loading) moved the whole model to GPU (model.to(weight_postprocess_device)), and bf16 + fp8 copies together exceed 4x45 GB. Fix: run the postprocessing per module with a GPU round-trip (peak = one layer's size):5. Persistent buffers (e.g. pruned
adaln_t_table) are never loadedload_model_from_full_model_state_dictmatches checkpoint keys againstnamed_parameters()only, andnamed_buffers()skips None-valued buffers (if buf is not Nonein torch's Module). A pruned checkpoint'sadaln_t_tabletherefore staysNoneand the model silently falls back to the sinusoidal time embedding with mismatched input dims (2688 vs 8). Fix: scan_buffersdirectly and materialize None buffers from the checkpoint files.6. K<16 linear layers under dynamic fp8
With dynamic FP8, small-K layers (e.g. pruned AdaLN with K=8) fail the cutlass 16-alignment check and fall into
triton_scaled_mm, which assertsweight.shape[0] == K— the weight must be[K, N]. Big layers (K=3072 etc.) take the cutlass path and are fine. If you must run tiny-K layers in fp8, transpose the weight before the triton call.Dynamic batching (concurrency)
With
--batching-mode dynamic --batching-max-size 4 --batching-delay-ms 1000, requests arriving within the aggregation window are packed into one denoise batch: 2 concurrent 768p/20-step requests ran at 21.5 GB/card peak (vs 14.3 GB for one), i.e. ~7 GB/card per extra request. We're measuring exact wall-clock throughput numbers now.Full patch summary (site-packages edits, 2026-08)
sglang/multimodal_gen/runtime/loader/fsdp_load.py_FP8_SCALE_REGISTRY+ fallback lookup in_maybe_dequantize_fp8(gotcha Add install with pip #3)sglang/multimodal_gen/runtime/models/dits/minimax_h3.py— pruned-format support:adaln_t_tableinterpolation inTimeEmbedder.forward, adaptiveadaln_input_dim, skip outer SiLU when the curve table is active (the curve already bakessilu(time_embedder(t)), matching ComfyUI'sapply_silu = not use_adaln_curves)Happy to turn any of these into proper PRs — especially #3 (scale registry) and #4 (per-module postprocessing) which look like general fixes for large-model dynamic-quant loading.
All reactions