Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@
"audio_channels": 2,
"keep_latents_dtype_in_scheduler": true,
"dit_quantized": true,
"dit_quant_scheme": "fp8-sgl",
"dit_quantized_ckpt": "/path/to/models/minimax_h3/h3_quantized/fp8/minimax_h3_fp8.safetensors",
"dit_quant_scheme": "fp8-f16-accum",
"dit_quantized_ckpt": "/path/to/models/minimax_h3/h3_quantized/minimax_h3_dit_fp8_f16_accum.safetensors",
"video_vae_quantized": true,
"video_vae_quant_scheme": "fp8-sgl",
"video_vae_quantized_ckpt": "/path/to/models/minimax_h3/h3_quantized/fp8/minimax_h3_video_vae_fp8_sgl_bias_fp16.safetensors",
"video_vae_quant_scheme": "fp8-f16-accum",
"video_vae_quantized_ckpt": "/path/to/models/minimax_h3/h3_quantized/minimax_h3_video_vae_fp8_f16_accum.safetensors",
"lora_dynamic_apply": true,
"lora_configs": [
{
Expand Down
69 changes: 68 additions & 1 deletion docs/EN/source/method_tutorials/quantization.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ LightX2V supports quantized inference for DIT, T5, and CLIP models, reducing mem
| `fp8-vllm` | FP8 channel symmetric | FP8 channel dynamic symmetric | [VLLM](https://github.com/vllm-project/vllm) | H100/H200/H800, RTX 40 series, etc. |
| `int8-vllm` | INT8 channel symmetric | INT8 channel dynamic symmetric | [VLLM](https://github.com/vllm-project/vllm) | A100/A800, RTX 30/40 series, etc. |
| `fp8-sgl` | FP8 channel symmetric | FP8 channel dynamic symmetric | [SGL](https://github.com/sgl-project/sglang/tree/main/sgl-kernel) | H100/H200/H800, RTX 40 series, etc. |
| `fp8-f16-accum` | FP8 channel symmetric | FP8 row-wise dynamic symmetric | CUTLASS FP16 accumulation | RTX 5090 (SM120) |
| `int8-sgl` | INT8 channel symmetric | INT8 channel dynamic symmetric | [SGL](https://github.com/sgl-project/sglang/tree/main/sgl-kernel) | A100/A800, RTX 30/40 series, etc. |
| `fp8-q8f` | FP8 channel symmetric | FP8 channel dynamic symmetric | [Q8-Kernels](https://github.com/KONAKONA666/q8_kernels) | RTX 40 series, L40S, etc. |
| `int8-q8f` | INT8 channel symmetric | INT8 channel dynamic symmetric | [Q8-Kernels](https://github.com/KONAKONA666/q8_kernels) | RTX 40 series, L40S, etc. |
Expand Down Expand Up @@ -67,7 +68,7 @@ For detailed quantization tool usage, refer to: [Model Conversion Documentation]

#### Supported Quantization Modes

DIT quantization modes (`dit_quant_scheme`) support: `fp8-vllm`, `int8-vllm`, `fp8-sgl`, `int8-sgl`, `fp8-q8f`, `int8-q8f`, `int8-torchao`, `int4-g128-marlin`, `fp8-b128-deepgemm`
DIT quantization modes (`dit_quant_scheme`) support: `fp8-vllm`, `int8-vllm`, `fp8-sgl`, `fp8-f16-accum`, `int8-sgl`, `fp8-q8f`, `int8-q8f`, `int8-torchao`, `int4-g128-marlin`, `fp8-b128-deepgemm`

#### Configuration Example

Expand All @@ -81,6 +82,72 @@ DIT quantization modes (`dit_quant_scheme`) support: `fp8-vllm`, `int8-vllm`, `f

> 💡 **Tip**: When there's only one DIT model in the script's `model_path`, `dit_quantized_ckpt` doesn't need to be specified separately.

#### MiniMax-H3 FP8 with FP16 Accumulation

On RTX 5090, MiniMax-H3 can use FP8 inputs with FP16 accumulation through `fp8-f16-accum`. Convert
the weights with the `h3-fp8-f16-accum` profile; regular `fp8-sgl` checkpoints are not compatible.
DiT and Video VAE decoder are converted separately. The profile selects the qmax-14 projections,
keeps standard FP8 quantization for the remaining layers, and records the policy in safetensors
metadata.

```bash
python tools/convert/converter.py \
--source /path/to/MiniMax-H3/transformer \
--output /path/to/h3_quantized \
--output_name minimax_h3_dit_fp8_f16_accum \
--output_ext .safetensors \
--model_type h3 \
--device cuda \
--quantized \
--bits 8 \
--linear_type fp8 \
--quantization_profile h3-fp8-f16-accum \
--single_file

python tools/convert/converter.py \
--source /path/to/MiniMax-H3/vae \
--output /path/to/h3_quantized \
--output_name minimax_h3_video_vae_fp8_f16_accum \
--output_ext .safetensors \
--model_type h3_video_vae_decoder \
--device cuda \
--quantized \
--bits 8 \
--linear_type fp8 \
--quantization_profile h3-fp8-f16-accum \
--single_file
```

```json
{
"dit_quantized": true,
"dit_quant_scheme": "fp8-f16-accum",
"dit_quantized_ckpt": "/path/to/minimax_h3_dit_fp8_f16_accum.safetensors",
"video_vae_quantized": true,
"video_vae_quant_scheme": "fp8-f16-accum",
"video_vae_quantized_ckpt": "/path/to/minimax_h3_video_vae_fp8_f16_accum.safetensors"
}
```

Activations use dynamic row-wise quantization with `scale = max(abs(x)) / qmax`. Reducing qmax
increases the scale and lowers the raw values accumulated in FP16, at the cost of fewer effective FP8
levels. In the validated MiniMax-H3 workload, DiT produced non-finite FFN-out values with qmax 14 and
12, while qmax 7 completed every denoising step. Video VAE decoder remained finite and had the lowest
error with qmax 14. The current H3 policy therefore fixes activation qmax to 7 for DiT and 14 for
Video VAE, avoiding mismatches between runtime configuration and checkpoint conversion.

The kernel is enabled only for DiT Q/K/V, attention output, and FFN projections, and for Video VAE
packed QKV, attention output, and FFN projections. It falls back to `fp8-sgl` when the extension is
unavailable or the device is not SM120. DiT tensor parallel also currently uses the `fp8-sgl` fallback.
Initialization logs report the effective scope or fallback reason. All other pipeline settings are
independent of this quantization mode.

The kernel automatically tunes its CUTLASS tile and swizzle for each exact GEMM shape. The first use
of an unseen shape benchmarks the built-in candidates in C++ and keeps the winner in a process-local
C++ cache; later calls in the same process perform only a cache lookup. Enabling `warmup` moves the
tuning cost out of the first request when warmup covers the production shapes. A restarted process
tunes its shapes again and does not write to the user's cache directory.

### T5 Model Quantization

#### Supported Quantization Modes
Expand Down
64 changes: 63 additions & 1 deletion docs/ZH_CN/source/method_tutorials/quantization.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ LightX2V 支持对 DIT、T5 和 CLIP 模型进行量化推理,通过降低模
| `fp8-vllm` | FP8 通道对称 | FP8 通道动态对称 | [VLLM](https://github.com/vllm-project/vllm) | H100/H200/H800, RTX 40系等 |
| `int8-vllm` | INT8 通道对称 | INT8 通道动态对称 | [VLLM](https://github.com/vllm-project/vllm) | A100/A800, RTX 30/40系等 |
| `fp8-sgl` | FP8 通道对称 | FP8 通道动态对称 | [SGL](https://github.com/sgl-project/sglang/tree/main/sgl-kernel) | H100/H200/H800, RTX 40系等 |
| `fp8-f16-accum` | FP8 通道对称 | FP8 行动态对称 | CUTLASS FP16 累加 | RTX 5090(SM120) |
| `int8-sgl` | INT8 通道对称 | INT8 通道动态对称 | [SGL](https://github.com/sgl-project/sglang/tree/main/sgl-kernel) | A100/A800, RTX 30/40系等 |
| `fp8-q8f` | FP8 通道对称 | FP8 通道动态对称 | [Q8-Kernels](https://github.com/KONAKONA666/q8_kernels) | RTX 40系, L40S等 |
| `int8-q8f` | INT8 通道对称 | INT8 通道动态对称 | [Q8-Kernels](https://github.com/KONAKONA666/q8_kernels) | RTX 40系, L40S等 |
Expand Down Expand Up @@ -67,7 +68,7 @@ huggingface-cli download lightx2v/Encoders-Lightx2v \

#### 支持的量化模式

DIT 量化模式(`dit_quant_scheme`)支持:`fp8-vllm`、`int8-vllm`、`fp8-sgl`、`int8-sgl`、`fp8-q8f`、`int8-q8f`、`int8-torchao`、`int4-g128-marlin`、`fp8-b128-deepgemm`
DIT 量化模式(`dit_quant_scheme`)支持:`fp8-vllm`、`int8-vllm`、`fp8-sgl`、`fp8-f16-accum`、`int8-sgl`、`fp8-q8f`、`int8-q8f`、`int8-torchao`、`int4-g128-marlin`、`fp8-b128-deepgemm`

#### 配置示例

Expand All @@ -81,6 +82,67 @@ DIT 量化模式(`dit_quant_scheme`)支持:`fp8-vllm`、`int8-vllm`、`fp8

> 💡 **提示**:当运行脚本的 `model_path` 中只有一个 DIT 模型时,`dit_quantized_ckpt` 可以不用单独指定。

#### MiniMax-H3 FP8 FP16 累加

RTX 5090 上的 MiniMax-H3 可以通过 `fp8-f16-accum` 使用 FP8 输入和 FP16 累加。权重需要用
`h3-fp8-f16-accum` profile 转换;普通 `fp8-sgl` checkpoint 不兼容。DiT 和 Video VAE decoder
分别转换,profile 会独立选择使用 qmax 14 的投影层、保留其他层的标准 FP8 量化,并把策略写入
safetensors metadata。

```bash
python tools/convert/converter.py \
--source /path/to/MiniMax-H3/transformer \
--output /path/to/h3_quantized \
--output_name minimax_h3_dit_fp8_f16_accum \
--output_ext .safetensors \
--model_type h3 \
--device cuda \
--quantized \
--bits 8 \
--linear_type fp8 \
--quantization_profile h3-fp8-f16-accum \
--single_file

python tools/convert/converter.py \
--source /path/to/MiniMax-H3/vae \
--output /path/to/h3_quantized \
--output_name minimax_h3_video_vae_fp8_f16_accum \
--output_ext .safetensors \
--model_type h3_video_vae_decoder \
--device cuda \
--quantized \
--bits 8 \
--linear_type fp8 \
--quantization_profile h3-fp8-f16-accum \
--single_file
```

```json
{
"dit_quantized": true,
"dit_quant_scheme": "fp8-f16-accum",
"dit_quantized_ckpt": "/path/to/minimax_h3_dit_fp8_f16_accum.safetensors",
"video_vae_quantized": true,
"video_vae_quant_scheme": "fp8-f16-accum",
"video_vae_quantized_ckpt": "/path/to/minimax_h3_video_vae_fp8_f16_accum.safetensors"
}
```

激活按行动态量化,`scale = max(abs(x)) / qmax`。减小 qmax 会扩大 scale,从而降低 FP16
累加器中的原始数值范围,但也会减少 FP8 有效量化级数。实测中 DiT 的 qmax 14 和 12 会在 FFN-out
产生非有限值,qmax 7 可完成全部去噪步骤;Video VAE decoder 在 qmax 14 下保持有限且误差最小。因此
当前 H3 策略固定使用 DiT activation qmax 7 和 Video VAE activation qmax 14,避免运行配置与
checkpoint 的转换策略错配。

DiT 仅对 Q/K/V、attention output 和 FFN projection 启用该内核,Video VAE 仅对 packed QKV、
attention output 和 FFN projection 启用。扩展不可用或设备不是 SM120 时会回退到 `fp8-sgl`;
DiT tensor parallel 当前也回退到 `fp8-sgl`。初始化日志会打印实际启用范围或回退原因。
其他 pipeline 配置与该量化模式相互独立。

该内核会按精确 GEMM shape 自动调优 CUTLASS tile 和 swizzle。首次遇到新 shape 时在 C++ 内遍历
内置候选,winner 保存在当前进程的 C++ cache 中;同一进程的后续调用只执行 cache 查询。若 warmup
覆盖正式请求的 shape,首次调优开销会在请求前完成。进程重启后会重新调优一次,不写用户目录。

### T5 模型量化

#### 支持的量化模式
Expand Down
49 changes: 49 additions & 0 deletions lightx2v/common/ops/mm/fp8_f16_accum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import math

import torch

from lightx2v.common.ops.mm.triton_kernels import fp8_quantize_range_triton

try:
from lightx2v_kernel.gemm import FP8_F16_ACCUM_MM_AVAILABLE, cutlass_scaled_fp8_mm_f16_accum
except ImportError:
FP8_F16_ACCUM_MM_AVAILABLE = False
cutlass_scaled_fp8_mm_f16_accum = None


def fp8_f16_accum_mm_unavailable_reason():
if not FP8_F16_ACCUM_MM_AVAILABLE:
return "the lightx2v-kernel extension does not provide the FP8-F16 accumulation op"
if not torch.cuda.is_available():
return "CUDA is unavailable"
capability = torch.cuda.get_device_capability()
if capability != (12, 0):
return f"SM120 is required, but the current CUDA capability is SM{capability[0]}{capability[1]}"
return None


def fp8_f16_accum_mm_available():
return fp8_f16_accum_mm_unavailable_reason() is None


def validate_fp8_f16_accum_qmax(activation_qmax):
activation_qmax = float(activation_qmax)
fp8_max = torch.finfo(torch.float8_e4m3fn).max
if not math.isfinite(activation_qmax) or not 0 < activation_qmax <= fp8_max:
raise ValueError(f"FP8 activation qmax must be finite and in (0, {fp8_max}], got {activation_qmax}")
return activation_qmax


def fp8_f16_accum_linear(input_tensor, weight, weight_scale, bias, activation_qmax):
input_shape = input_tensor.shape
input_matrix = input_tensor.reshape(-1, input_shape[-1])
quantized, activation_scale = fp8_quantize_range_triton(input_matrix, activation_qmax)
output = cutlass_scaled_fp8_mm_f16_accum(
quantized,
weight,
activation_scale,
weight_scale.float(),
input_tensor.dtype,
bias,
)
return output.view(*input_shape[:-1], weight.shape[1])
32 changes: 32 additions & 0 deletions lightx2v/common/ops/mm/mm_weight.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
except ImportError:
magi_register_custom_op = None

from lightx2v.common.ops.mm.fp8_f16_accum import (
fp8_f16_accum_linear,
fp8_f16_accum_mm_available,
validate_fp8_f16_accum_qmax,
)
from lightx2v.common.ops.mm.sgl_kernel import sgl_fp8_scaled_mm, sgl_fp8_scaled_mm_meta
from lightx2v.common.ops.mm.triton_kernels import (
fp8_gemm_bias_triton,
Expand Down Expand Up @@ -1977,6 +1982,33 @@ def apply(self, input_tensor):
return output_tensor


@MM_WEIGHT_REGISTER("fp8-f16-accum")
class MMWeightWfp8channelAfp8channelF16Accum(MMWeightWfp8channelAfp8channeldynamicSgl):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fp8_activation_qmax = None

def enable_fp8_f16_accum(self, activation_qmax):
activation_qmax = validate_fp8_f16_accum_qmax(activation_qmax)
if fp8_f16_accum_mm_available():
self.fp8_activation_qmax = activation_qmax

def apply(self, input_tensor):
if self.fp8_activation_qmax is None:
return super().apply(input_tensor)

output_tensor = fp8_f16_accum_linear(
input_tensor,
self.weight,
self.weight_scale,
self._get_actual_bias(),
self.fp8_activation_qmax,
)
if self.has_lora_branch:
return output_tensor + self.apply_lora(input_tensor)
return output_tensor


@MM_WEIGHT_REGISTER("int8-sgl")
class MMWeightWint8channelAint8channeldynamicSglActVllm(MMWeightQuantTemplate):
"""
Expand Down
18 changes: 18 additions & 0 deletions lightx2v/common/ops/mm/triton_kernels.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,24 @@ def fp8_quantize_triton(x):
return quantized.view(x_shape_orig), scales.view(x_shape_orig[:-1])


def fp8_quantize_range_triton(x, qmax):
x_shape = x.shape
x = x.reshape(-1, x_shape[-1]).contiguous()
quantized = torch.empty_like(x, dtype=torch.float8_e4m3fn)
scales = torch.empty(x.shape[0], dtype=torch.float32, device=x.device)
block_size = next_power_of_2(x_shape[-1])
fp8_quantize_kernel[(x.shape[0],)](
x,
quantized,
scales,
x_shape[-1],
block_size,
FP8_MAX_VAL=qmax,
num_warps=8,
)
return quantized.view(x_shape), scales.view(x_shape[:-1])


def upcast_if_fp8(a):
if "fp8" in str(a):
return torch.float16
Expand Down
27 changes: 27 additions & 0 deletions lightx2v/models/input_encoders/hf/q_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
except ImportError:
fp8_linear = None

from lightx2v.common.ops.mm.fp8_f16_accum import (
fp8_f16_accum_linear,
fp8_f16_accum_mm_available,
validate_fp8_f16_accum_qmax,
)
from lightx2v.common.ops.mm.sgl_kernel import sgl_fp8_scaled_mm
from lightx2v.common.ops.mm.triton_kernels import fp8_gemm_bias_triton, fp8_gemm_triton, fp8_quantize_triton, int8_gemm_bias_triton, int8_gemm_triton, int8_quantize_triton
from lightx2v_platform.ops.mm.mthreads_musa.fp8_scaled_mm import fp8_linear as musa_fp8_linear
Expand Down Expand Up @@ -310,6 +315,28 @@ def maybe_cast(t):
return self


class F16AccumQuantLinearFp8(SglQuantLinearFp8):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fp8_activation_qmax = None

def enable_fp8_f16_accum(self, activation_qmax):
activation_qmax = validate_fp8_f16_accum_qmax(activation_qmax)
if fp8_f16_accum_mm_available():
self.fp8_activation_qmax = activation_qmax

def forward(self, input_tensor):
if self.fp8_activation_qmax is None:
return super().forward(input_tensor)
return fp8_f16_accum_linear(
input_tensor,
self.weight.t(),
self.weight_scale,
self.bias,
self.fp8_activation_qmax,
)


class MusaQuantLinearFp8(nn.Module):
"""MUSA W8A8 FP8 linear with per-channel weights and per-token inputs."""

Expand Down
1 change: 1 addition & 0 deletions lightx2v/models/networks/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def _check_dit_quantized(self):
"int8-q8f",
"int8-convrot",
"fp8-b128-deepgemm",
"fp8-f16-accum",
"fp8-sgl",
"int8-sgl",
"int8-torchao",
Expand Down
36 changes: 36 additions & 0 deletions lightx2v/models/networks/minimax_h3/fp8_f16_accum_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from pathlib import Path

from safetensors import safe_open

FP8_F16_ACCUM_WEIGHT_QMAX = 14.0
DIT_FP8_F16_ACCUM_ACTIVATION_QMAX = 7.0
VIDEO_VAE_FP8_F16_ACCUM_ACTIVATION_QMAX = 14.0
FP8_F16_ACCUM_QUANTIZATION_PROFILE = "h3-fp8-f16-accum"
FP8_F16_ACCUM_PROJECTION_SUFFIXES = (
".attn.to_q",
".attn.to_k",
".attn.to_v",
".attn.to_out.0",
".ff.net.0.proj",
".ff.net.2",
)


def validate_fp8_f16_accum_checkpoint(checkpoint_path):
checkpoint_path = Path(checkpoint_path)
files = (checkpoint_path,) if checkpoint_path.is_file() else tuple(sorted(checkpoint_path.glob("*.safetensors")))
if not files:
raise FileNotFoundError(f"No safetensors weights found in FP8 checkpoint: {checkpoint_path}")

for filename in files:
with safe_open(filename, framework="pt", device="cpu") as checkpoint:
metadata = checkpoint.metadata() or {}
profile = metadata.get("quantization_profile")
if profile != FP8_F16_ACCUM_QUANTIZATION_PROFILE:
raise ValueError(f"{filename} requires quantization profile {FP8_F16_ACCUM_QUANTIZATION_PROFILE!r}, got {profile!r}")
try:
weight_qmax = float(metadata.get("weight_qmax"))
except (TypeError, ValueError):
weight_qmax = None
if weight_qmax != FP8_F16_ACCUM_WEIGHT_QMAX:
raise ValueError(f"{filename} requires weight_qmax={FP8_F16_ACCUM_WEIGHT_QMAX}, got {metadata.get('weight_qmax')!r}")
Loading
Loading