[Diffusion] modelopt diffusion fp8 support for flux1/flux2 and wan2.2 (#22365)
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.multimodal_gen.runtime.layers.quantization.modelopt_quant import (
|
||||
ModelOptFp8Config,
|
||||
ModelOptFp8LinearMethod,
|
||||
)
|
||||
from sglang.srt.layers.quantization.fp8_kernel import static_quant_fp8
|
||||
from sglang.srt.layers.quantization.fp8_utils import (
|
||||
cutlass_fp8_supported,
|
||||
input_to_float8,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=20, suite="stage-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=80, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
DEVICE = "cuda"
|
||||
DTYPE = torch.bfloat16
|
||||
MAX_FP8_DIFF = 5e-4
|
||||
TEST_CASES = [
|
||||
pytest.param(19, 150, 80, id="misaligned_projection_shape"),
|
||||
pytest.param(512, 3072, 4096, id="flux2_added_kv_projection_shape"),
|
||||
]
|
||||
|
||||
|
||||
def _modelopt_fp8_supported() -> bool:
|
||||
return torch.cuda.is_available() and cutlass_fp8_supported()
|
||||
|
||||
|
||||
def _calc_diff(x: torch.Tensor, y: torch.Tensor) -> float:
|
||||
x, y = x.double(), y.double()
|
||||
denominator = (x * x + y * y).sum()
|
||||
if denominator == 0:
|
||||
return 0.0
|
||||
sim = 2 * (x * y).sum() / denominator
|
||||
return (1 - sim).item()
|
||||
|
||||
|
||||
def _dequantize_fp8_input(qinput: torch.Tensor, x_scale: torch.Tensor) -> torch.Tensor:
|
||||
return qinput.to(torch.float32) * x_scale.to(torch.float32)
|
||||
|
||||
|
||||
def _dequantize_fp8_weight(
|
||||
weight: torch.Tensor, weight_scale: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
if weight_scale.ndim == 0 or weight_scale.numel() == 1:
|
||||
scale = weight_scale.to(torch.float32)
|
||||
else:
|
||||
scale = weight_scale.to(torch.float32).reshape(-1, 1).t()
|
||||
return weight.to(torch.float32) * scale
|
||||
|
||||
|
||||
def _build_layer(
|
||||
weight_q: torch.Tensor,
|
||||
weight_scale: torch.Tensor,
|
||||
input_scale: torch.Tensor,
|
||||
) -> tuple[torch.nn.Module, ModelOptFp8LinearMethod]:
|
||||
output_size, input_size = weight_q.shape
|
||||
method = ModelOptFp8LinearMethod(
|
||||
ModelOptFp8Config(is_checkpoint_fp8_serialized=True)
|
||||
)
|
||||
layer = torch.nn.Module()
|
||||
method.create_weights(
|
||||
layer=layer,
|
||||
input_size_per_partition=input_size,
|
||||
output_partition_sizes=[output_size],
|
||||
input_size=input_size,
|
||||
output_size=output_size,
|
||||
params_dtype=DTYPE,
|
||||
weight_loader=lambda *args, **kwargs: None,
|
||||
)
|
||||
layer = layer.to(device=DEVICE)
|
||||
|
||||
layer.weight.data.copy_(weight_q)
|
||||
layer.weight_scale.data.copy_(weight_scale.reshape_as(layer.weight_scale))
|
||||
layer.input_scale.data.copy_(input_scale.reshape_as(layer.input_scale))
|
||||
method.process_weights_after_loading(layer)
|
||||
return layer, method
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _modelopt_fp8_supported(),
|
||||
reason="Diffusion ModelOpt FP8 scaled mm correctness requires CUDA FP8 support",
|
||||
)
|
||||
@pytest.mark.parametrize("m,n,k", TEST_CASES)
|
||||
def test_checkpoint_processing(m: int, n: int, k: int) -> None:
|
||||
generator = torch.Generator(device=DEVICE)
|
||||
generator.manual_seed(20260410 + m + n + k)
|
||||
|
||||
weight = torch.randn((n, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
weight_q, weight_scale = input_to_float8(weight)
|
||||
input_scale = torch.tensor(1.0, device=DEVICE, dtype=torch.float32)
|
||||
|
||||
layer, _ = _build_layer(weight_q, weight_scale, input_scale)
|
||||
|
||||
assert tuple(layer.weight.shape) == (k, n)
|
||||
assert tuple(layer.weight.stride()) == (1, k)
|
||||
assert layer.weight.dtype == torch.float8_e4m3fn
|
||||
assert layer.input_scale.ndim == 0
|
||||
assert tuple(layer.weight_scale.shape) == (n, 1)
|
||||
|
||||
expected_weight = weight_q.t().to(torch.float32) * weight_scale.to(torch.float32)
|
||||
actual_weight = _dequantize_fp8_weight(layer.weight, layer.weight_scale)
|
||||
torch.testing.assert_close(actual_weight, expected_weight, atol=0.0, rtol=0.0)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _modelopt_fp8_supported(),
|
||||
reason="Diffusion ModelOpt FP8 scaled mm correctness requires CUDA FP8 support",
|
||||
)
|
||||
@pytest.mark.parametrize("m,n,k", TEST_CASES)
|
||||
def test_shape_correctness(m: int, n: int, k: int) -> None:
|
||||
generator = torch.Generator(device=DEVICE)
|
||||
generator.manual_seed(20260410 + m + n + k)
|
||||
|
||||
x = torch.randn((m, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
weight = torch.randn((n, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
weight_q, weight_scale = input_to_float8(weight)
|
||||
_, input_scale = input_to_float8(x)
|
||||
|
||||
layer, method = _build_layer(weight_q, weight_scale, input_scale)
|
||||
|
||||
qinput, x_scale = static_quant_fp8(
|
||||
x.contiguous(),
|
||||
layer.input_scale,
|
||||
repeat_scale=method.cutlass_fp8_supported,
|
||||
)
|
||||
expected = torch.matmul(
|
||||
_dequantize_fp8_input(qinput, x_scale),
|
||||
_dequantize_fp8_weight(layer.weight, layer.weight_scale),
|
||||
)
|
||||
|
||||
actual = method.apply(layer, x)
|
||||
diff = _calc_diff(actual, expected.to(dtype=DTYPE))
|
||||
assert diff < MAX_FP8_DIFF, f"{m=}, {n=}, {k=}, {diff=:.6f}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -0,0 +1,308 @@
|
||||
---
|
||||
name: sglang-diffusion-modelopt-quant
|
||||
description: Use when quantizing a diffusion DiT with NVIDIA ModelOpt and making the resulting FP8 or NVFP4 checkpoint loadable, verifiable, and benchmarkable in SGLang Diffusion.
|
||||
---
|
||||
|
||||
# SGLang Diffusion ModelOpt Quant
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill when the task is to take a diffusion transformer through the full ModelOpt workflow:
|
||||
|
||||
- quantize it with NVIDIA ModelOpt
|
||||
- adapt the exported checkpoint to SGLang Diffusion
|
||||
- verify that quality holds up
|
||||
- benchmark whether the quantized checkpoint is actually faster
|
||||
|
||||
This skill owns the ModelOpt-to-SGLang bridge. It is not a generic kernel-tuning skill.
|
||||
|
||||
## Core Rules
|
||||
|
||||
- Use ModelOpt's official `quantize.py` as the PTQ source of truth.
|
||||
- Keep the workflow generic. Put model-specific fallback logic in small isolated branches, not in the main conversion path.
|
||||
- Benchmark only when BF16 and quantized commands are identical except for the checkpoint override being tested.
|
||||
- For diffusion FP8, pin `dit_cpu_offload=false` and `dit_layerwise_offload=false`.
|
||||
- For multi-transformer pipelines, use per-component overrides when different components need different checkpoints.
|
||||
- When a branch is missing the validated helper tools, refresh `python/sglang/multimodal_gen/tools/convert_modelopt_fp8_checkpoint.py` and `python/sglang/multimodal_gen/tools/compare_diffusion_trajectory_similarity.py` instead of inventing one-off scripts elsewhere.
|
||||
- After validating a new ModelOpt quant path, update both the FP8 and NVFP4 support tables in this skill before closing the task.
|
||||
|
||||
## Read First
|
||||
|
||||
Read these sources before changing code:
|
||||
|
||||
- NVIDIA ModelOpt diffusers guide: `examples/diffusers/README.md`
|
||||
- ModelOpt quantization entrypoint: `examples/diffusers/quantization/quantize.py`
|
||||
- ModelOpt diffusers quant presets: `examples/diffusers/quantization/config.py`
|
||||
- SGLang diffusion quant runtime:
|
||||
- `python/sglang/multimodal_gen/runtime/layers/quantization/modelopt_quant.py`
|
||||
- `python/sglang/multimodal_gen/runtime/utils/quantization_utils.py`
|
||||
- `python/sglang/multimodal_gen/runtime/loader/transformer_load_utils.py`
|
||||
- Helper tools in this repo:
|
||||
- [`python/sglang/multimodal_gen/tools/convert_modelopt_fp8_checkpoint.py`](../../../tools/convert_modelopt_fp8_checkpoint.py)
|
||||
- [`python/sglang/multimodal_gen/tools/compare_diffusion_trajectory_similarity.py`](../../../tools/compare_diffusion_trajectory_similarity.py)
|
||||
|
||||
If you are working on a new model family, inspect the transformer's config and tensor naming before changing the generic converter.
|
||||
|
||||
## What SGLang Supports Here
|
||||
|
||||
This repo now contains:
|
||||
|
||||
- flat `quant_method=modelopt` plus `quant_algo=FP8/NVFP4` resolution
|
||||
- diffusion-side ModelOpt FP8 linear loading
|
||||
- diffusion-side NVFP4 loading from ModelOpt exports
|
||||
- FLUX.2 packed-QKV detection that distinguishes packed NVFP4 checkpoints from standard diffusers exports
|
||||
- automatic protection against incompatible FP8 offload modes
|
||||
- FP8 export conversion:
|
||||
[`python/sglang/multimodal_gen/tools/convert_modelopt_fp8_checkpoint.py`](../../../tools/convert_modelopt_fp8_checkpoint.py)
|
||||
- trajectory similarity validation:
|
||||
[`python/sglang/multimodal_gen/tools/compare_diffusion_trajectory_similarity.py`](../../../tools/compare_diffusion_trajectory_similarity.py)
|
||||
|
||||
## Documentation Maintenance
|
||||
|
||||
- Keep two separate support tables in this skill: one for FP8 and one for NVFP4.
|
||||
- After finishing a new quant support path, update both tables in every mirrored copy of this skill.
|
||||
- Each row must record the validated scope, the Hugging Face repo or path for the quantized DiT weights, and the key caveats.
|
||||
- If the quantized DiT weights are not published yet, write `unpublished` explicitly instead of leaving the field blank.
|
||||
|
||||
## FP8 Supported Models
|
||||
|
||||
| Base Model | Validated Scope | HF DiT Weights | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `black-forest-labs/FLUX.1-dev` | single-transformer override, deterministic latent/image comparison, H100 benchmark, torch-profiler trace | `BBuf/flux1-dev-modelopt-fp8-sglang-transformer` | SGLang converter keeps a validated BF16 fallback set for modulation and FF projection layers; use `--model-id FLUX.1-dev` for local mirrors |
|
||||
| `black-forest-labs/FLUX.2-dev` | single-transformer override load and generation path | `BBuf/flux2-dev-modelopt-fp8-sglang-transformer` | published SGLang-ready transformer override |
|
||||
| `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | primary `transformer` quantized, `transformer_2` kept BF16 | `BBuf/wan22-t2v-a14b-modelopt-fp8-sglang-transformer` | do not describe this as dual-transformer full-model FP8 unless that path is validated separately |
|
||||
|
||||
## NVFP4 Supported Models
|
||||
|
||||
| Base Model | Validated Scope | HF DiT Weights | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `black-forest-labs/FLUX.2-dev` | packed-QKV load path | `black-forest-labs/FLUX.2-dev-NVFP4` | validated packed export detection and runtime layout handling |
|
||||
|
||||
## FP8 Vs NVFP4
|
||||
|
||||
FP8 and NVFP4 are not wired into SGLang in exactly the same way.
|
||||
|
||||
FP8:
|
||||
|
||||
- the validated ModelOpt diffusers FP8 export still needs an extra SGLang-side conversion step
|
||||
- SGLang expects explicit `weight_scale` and `input_scale`
|
||||
- the validated path also materializes SGLang-native `float8_e4m3fn` weights from `backbone.pt`
|
||||
|
||||
NVFP4:
|
||||
|
||||
- the official diffusers export often already contains packed FP4 weights, scale tensors, and enough safetensors metadata for SGLang to rebuild the quant config
|
||||
- in that case SGLang mainly needs to detect the checkpoint family and rearrange tensors into the runtime layout
|
||||
- this is why NVFP4 often does not need an extra offline conversion pass like FP8 does
|
||||
|
||||
Important caveat:
|
||||
|
||||
- "often" does not mean "always"
|
||||
- the exact load path still depends on the checkpoint family, especially whether a model uses a packed-QKV layout
|
||||
|
||||
## Generic Workflow
|
||||
|
||||
### 1. Verify The BF16 Baseline First
|
||||
|
||||
Before quantizing anything:
|
||||
|
||||
- run the original BF16 model in SGLang
|
||||
- fix the prompt, seed, size, step count, and GPU topology
|
||||
- save the output and `perf.json`
|
||||
|
||||
Do not start quantization work until the BF16 path is already healthy.
|
||||
|
||||
### 2. Quantize With Official ModelOpt
|
||||
|
||||
Use ModelOpt's official script. Generic template:
|
||||
|
||||
```bash
|
||||
python quantize.py \
|
||||
--model <model-name> \
|
||||
--override-model-path <hf-repo-or-local-model> \
|
||||
--model-dtype <Half|BFloat16> \
|
||||
--format <fp8|nvfp4> \
|
||||
--batch-size 1 \
|
||||
--calib-size <calib-size> \
|
||||
--n-steps <calib-steps> \
|
||||
--quantize-mha \
|
||||
--prompts-file <prompt-file> \
|
||||
--quantized-torch-ckpt-save-path <out>/ckpt \
|
||||
--hf-ckpt-dir <out>/hf
|
||||
```
|
||||
|
||||
For multi-transformer models:
|
||||
|
||||
- quantize each backbone deliberately
|
||||
- keep each output directory separate
|
||||
- save both `backbone.pt` and the matching `hf/<component>` export
|
||||
|
||||
### 3. Convert FP8 Exports For SGLang
|
||||
|
||||
FP8 requires an extra conversion step:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=python python3 -m sglang.multimodal_gen.tools.convert_modelopt_fp8_checkpoint \
|
||||
--modelopt-hf-dir <out>/hf \
|
||||
--modelopt-backbone-ckpt <out>/ckpt/backbone.pt \
|
||||
--base-transformer-dir <base-model-transformer-dir> \
|
||||
--output-dir <out>/sglang_transformer \
|
||||
--overwrite
|
||||
```
|
||||
|
||||
What the converter does:
|
||||
|
||||
- reads `weight_quantizer._amax` and `input_quantizer._amax` from `backbone.pt`
|
||||
- writes `weight_scale` and `input_scale`
|
||||
- materializes eligible FP8 weights as `float8_e4m3fn`
|
||||
- preserves ModelOpt `ignore` layers as BF16
|
||||
- strips stale `_quantizer.*` tensors and fallback-layer scales that should not survive into the SGLang-native checkpoint
|
||||
|
||||
For `FLUX.1-dev`, the validated fallback set currently keeps these modules in BF16:
|
||||
|
||||
- `transformer_blocks.*.norm1.linear`
|
||||
- `transformer_blocks.*.norm1_context.linear`
|
||||
- `transformer_blocks.*.ff.net.0.proj`
|
||||
- `transformer_blocks.*.ff.net.2`
|
||||
- `transformer_blocks.*.ff_context.net.0.proj`
|
||||
- `transformer_blocks.*.ff_context.net.2`
|
||||
- `single_transformer_blocks.*.norm.linear`
|
||||
|
||||
Use `--model-type flux1` to force that profile, or rely on `--model-type auto` when the export config identifies `FluxTransformer2DModel`.
|
||||
|
||||
### 4. Load The Quantized Checkpoint In SGLang
|
||||
|
||||
Single-transformer example:
|
||||
|
||||
```bash
|
||||
sglang generate \
|
||||
--model-path <base-model> \
|
||||
--transformer-path <quantized-transformer> \
|
||||
--prompt "<prompt>" \
|
||||
--seed <seed> \
|
||||
--save-output
|
||||
```
|
||||
|
||||
Multi-transformer example:
|
||||
|
||||
```bash
|
||||
sglang generate \
|
||||
--model-path <base-model> \
|
||||
--transformer-path <quantized-transformer> \
|
||||
--transformer-2-path <another-transformer-or-bf16-override> \
|
||||
--prompt "<prompt>" \
|
||||
--seed <seed> \
|
||||
--save-output
|
||||
```
|
||||
|
||||
Guideline:
|
||||
|
||||
- use the global `--transformer-path` only when the model effectively has one transformer override to apply
|
||||
- use per-component overrides when different backbones need different checkpoints
|
||||
- the preferred CLI form is `--<component>-path`
|
||||
- config-expanded forms such as `--component_paths.transformer_2=...` also resolve to the same internal override map
|
||||
|
||||
### 5. Validate Accuracy
|
||||
|
||||
Use two levels of validation.
|
||||
|
||||
Reduced deterministic validation:
|
||||
|
||||
- keep prompt, seed, resolution, and step count fixed
|
||||
- compare BF16 and quantized runs
|
||||
- capture denoising trajectories
|
||||
- inspect per-step latent cosine similarity plus MAE or RMSE
|
||||
- compare final frames with image metrics such as PSNR or MAE
|
||||
|
||||
Tool:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=python python3 -m sglang.multimodal_gen.tools.compare_diffusion_trajectory_similarity \
|
||||
--model-path <base-model> \
|
||||
--model-id <optional-native-model-id> \
|
||||
--prompt "<prompt>" \
|
||||
--width <w> \
|
||||
--height <h> \
|
||||
--num-inference-steps <steps> \
|
||||
--guidance-scale <cfg> \
|
||||
--seed <seed> \
|
||||
--candidate-transformer-path <quantized-transformer> \
|
||||
--output-json <report.json>
|
||||
```
|
||||
|
||||
Use `--model-id FLUX.1-dev` when `--model-path` points to a local directory but the runtime still needs the native FLUX.1 model registration.
|
||||
|
||||
Full-output validation:
|
||||
|
||||
- run the same user-facing generation config in BF16 and quantized mode
|
||||
- inspect the output visually
|
||||
- only claim "quality preserved" for the exact scope you actually checked
|
||||
|
||||
### 6. Benchmark Correctly
|
||||
|
||||
Benchmark only when these match between BF16 and quantized:
|
||||
|
||||
- prompt
|
||||
- seed
|
||||
- width and height
|
||||
- frame count
|
||||
- inference step count
|
||||
- GPU count and topology
|
||||
- offload flags
|
||||
- compile settings
|
||||
- profiler settings
|
||||
|
||||
Only the quantized checkpoint path should differ.
|
||||
|
||||
Interpretation rule:
|
||||
|
||||
- the primary expected gain is in denoising
|
||||
- text-encoding and VAE differences are secondary and should not be over-attributed unless they were quantized too
|
||||
|
||||
### 7. Add Model-Specific Fallbacks Only When Needed
|
||||
|
||||
If the generic FP8 path fails on a new model family:
|
||||
|
||||
- inspect which modules are numerically sensitive or loader-incompatible
|
||||
- keep fallback patterns small and explicit
|
||||
- isolate them in the converter instead of scattering ad-hoc exceptions
|
||||
- re-run deterministic trajectory checks after every fallback change
|
||||
|
||||
Do not turn one validated model quirk into a generic rule unless another family also needs it.
|
||||
|
||||
## FP8 Offload Constraint
|
||||
|
||||
Current diffusion ModelOpt FP8 support requires:
|
||||
|
||||
- `dit_cpu_offload=false`
|
||||
- `dit_layerwise_offload=false`
|
||||
|
||||
Reason:
|
||||
|
||||
- the FP8 linear path depends on a CUTLASS-compatible weight layout after loading
|
||||
- the offload and restore path does not preserve that layout
|
||||
- in particular, layerwise offload can flatten and rebuild FP8 weights in a way that breaks the column-major requirement used by the FP8 GEMM path
|
||||
|
||||
Runtime behavior:
|
||||
|
||||
- SGLang currently force-disables these two flags when it detects `modelopt_fp8`
|
||||
- benchmark commands should still pin them explicitly so the command line itself makes the comparison rule obvious
|
||||
|
||||
## Claim Discipline
|
||||
|
||||
When documenting results:
|
||||
|
||||
- claim only scopes that were actually validated end to end
|
||||
- do not collapse "single-transformer FP8 override" into "full-model FP8"
|
||||
- do not call a practical deployment comparison a benchmark if BF16 and quantized commands used different offload behavior
|
||||
|
||||
## Current Code Areas
|
||||
|
||||
| File | Role |
|
||||
| --- | --- |
|
||||
| `runtime/layers/quantization/__init__.py` | registers diffusion quant methods |
|
||||
| `runtime/layers/quantization/modelopt_quant.py` | ModelOpt FP8 and NVFP4 runtime loading |
|
||||
| `runtime/utils/quantization_utils.py` | resolves flat ModelOpt configs and reconstructs NVFP4 config from metadata |
|
||||
| `runtime/loader/transformer_load_utils.py` | guards incompatible FP8 offload modes |
|
||||
| `runtime/models/dits/flux_2.py` | packed-QKV handling for the packed FLUX.2 NVFP4 family |
|
||||
| `tools/convert_modelopt_fp8_checkpoint.py` | FP8 offline conversion into SGLang-native layout |
|
||||
| `tools/compare_diffusion_trajectory_similarity.py` | reduced deterministic BF16-vs-quantized validation |
|
||||
@@ -8,15 +8,17 @@ from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config impor
|
||||
from sglang.multimodal_gen.runtime.layers.quantization.fp8 import Fp8Config
|
||||
from sglang.multimodal_gen.runtime.layers.quantization.modelopt_quant import (
|
||||
ModelOptFp4Config,
|
||||
ModelOptFp8Config,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.quantization.modelslim import ModelSlimConfig
|
||||
|
||||
QuantizationMethods = Literal["fp8", "modelopt_fp4", "modelslim"]
|
||||
QuantizationMethods = Literal["fp8", "modelopt_fp8", "modelopt_fp4", "modelslim"]
|
||||
|
||||
QUANTIZATION_METHODS: list[str] = list(get_args(QuantizationMethods))
|
||||
|
||||
# The customized quantization methods which will be added to this dict.
|
||||
_CUSTOMIZED_METHOD_TO_QUANT_CONFIG = {
|
||||
"modelopt_fp8": ModelOptFp8Config,
|
||||
"modelopt_fp4": ModelOptFp4Config,
|
||||
"modelslim": ModelSlimConfig,
|
||||
"fp8": Fp8Config,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@@ -21,12 +22,20 @@ from sglang.multimodal_gen.runtime.models.parameter import (
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.utils import set_weight_attrs
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
from sglang.srt.layers.quantization.fp8_utils import (
|
||||
apply_fp8_linear,
|
||||
cutlass_fp8_supported,
|
||||
)
|
||||
from sglang.srt.layers.quantization.modelopt_quant import (
|
||||
pad_nvfp4_activation_for_cutlass,
|
||||
pad_nvfp4_weight,
|
||||
slice_nvfp4_output,
|
||||
)
|
||||
from sglang.srt.layers.quantization.utils import is_layer_skipped
|
||||
from sglang.srt.layers.quantization.utils import (
|
||||
convert_to_channelwise,
|
||||
is_layer_skipped,
|
||||
requantize_with_max_scale,
|
||||
)
|
||||
from sglang.srt.layers.utils.common import copy_or_rebind_param
|
||||
from sglang.srt.utils.common import round_up
|
||||
|
||||
@@ -82,15 +91,84 @@ class ModelOptQuantConfig(QuantizationConfig):
|
||||
def override_quantization_method(cls, hf_quant_config, user_quant) -> Optional[str]:
|
||||
if hf_quant_config is None:
|
||||
return None
|
||||
quant_algo = hf_quant_config.get("quant_algo", "").upper()
|
||||
if user_quant == "modelopt":
|
||||
if not ("NVFP4" in quant_algo or "FP4" in quant_algo):
|
||||
logger.warning(
|
||||
f"Unsupported quant_algo '{quant_algo}' for 'modelopt'; defaulting to modelopt_fp4."
|
||||
)
|
||||
|
||||
quant_algo = (
|
||||
hf_quant_config.get("quant_algo")
|
||||
or hf_quant_config.get("quantization", {}).get("quant_algo")
|
||||
or ""
|
||||
).upper()
|
||||
if user_quant in {"modelopt", "modelopt_fp8"} and "FP8" in quant_algo:
|
||||
return "modelopt_fp8"
|
||||
if user_quant in {"modelopt", "modelopt_fp4"} and (
|
||||
"NVFP4" in quant_algo or "FP4" in quant_algo
|
||||
):
|
||||
return "modelopt_fp4"
|
||||
return None
|
||||
|
||||
def is_layer_excluded(self, prefix: str) -> bool:
|
||||
for pattern in self.exclude_modules:
|
||||
regex_str = re.escape(pattern).replace(r"\*", r".*")
|
||||
if re.fullmatch(regex_str, prefix):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ModelOptFp8Config(ModelOptQuantConfig):
|
||||
"""Config class for ModelOpt FP8 diffusion checkpoints."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_checkpoint_fp8_serialized: bool = False,
|
||||
exclude_modules: Optional[List[str]] = None,
|
||||
packed_modules_mapping: Optional[Dict[str, List[str]]] = None,
|
||||
) -> None:
|
||||
super().__init__(exclude_modules, packed_modules_mapping)
|
||||
self.is_checkpoint_fp8_serialized = is_checkpoint_fp8_serialized
|
||||
if is_checkpoint_fp8_serialized:
|
||||
logger.warning(
|
||||
"Detected ModelOpt FP8 checkpoint. The format is experimental and subject to change."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_name(cls) -> str:
|
||||
return "modelopt_fp8"
|
||||
|
||||
@classmethod
|
||||
def get_supported_act_dtypes(cls) -> List[torch.dtype]:
|
||||
return [torch.bfloat16, torch.half]
|
||||
|
||||
@classmethod
|
||||
def get_min_capability(cls) -> int:
|
||||
return 89
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: Dict[str, Any]) -> "ModelOptFp8Config":
|
||||
quant_method = config.get("quant_algo")
|
||||
exclude_modules = config.get("ignore")
|
||||
if quant_method is None:
|
||||
try:
|
||||
quantization_section = cls.get_from_keys(config, ["quantization"])
|
||||
quant_method = quantization_section.get("quant_algo")
|
||||
exclude_modules = quantization_section.get("exclude_modules")
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
"Cannot find 'quant_algo' in the model's quantization config."
|
||||
) from exc
|
||||
|
||||
if quant_method is None or "FP8" not in quant_method:
|
||||
raise ValueError(
|
||||
"ModelOptFp8Config only supports static FP8 quantization in SGLang diffusion."
|
||||
)
|
||||
|
||||
return cls(
|
||||
is_checkpoint_fp8_serialized=True,
|
||||
exclude_modules=exclude_modules,
|
||||
packed_modules_mapping=config.get("packed_modules_mapping"),
|
||||
)
|
||||
|
||||
def get_quant_method(self, layer: torch.nn.Module, prefix: str):
|
||||
return self._get_quant_method(layer, prefix, Linear=ModelOptFp8LinearMethod)
|
||||
|
||||
|
||||
class ModelOptFp4Config(ModelOptQuantConfig):
|
||||
"""Config class for NVFP4."""
|
||||
@@ -101,6 +179,7 @@ class ModelOptFp4Config(ModelOptQuantConfig):
|
||||
group_size: int = None,
|
||||
exclude_modules: List[str] = None,
|
||||
packed_modules_mapping: Optional[Dict[str, List[str]]] = None,
|
||||
checkpoint_uses_packed_qkv: bool = False,
|
||||
) -> None:
|
||||
super().__init__(exclude_modules, packed_modules_mapping)
|
||||
self.is_checkpoint_nvfp4_serialized = is_checkpoint_nvfp4_serialized
|
||||
@@ -110,6 +189,7 @@ class ModelOptFp4Config(ModelOptQuantConfig):
|
||||
"format is experimental and subject to change."
|
||||
)
|
||||
self.group_size = group_size
|
||||
self.checkpoint_uses_packed_qkv = checkpoint_uses_packed_qkv
|
||||
|
||||
@classmethod
|
||||
def get_name(cls) -> str:
|
||||
@@ -193,30 +273,100 @@ class ModelOptFp4Config(ModelOptQuantConfig):
|
||||
group_size=group_size,
|
||||
exclude_modules=exclude_modules,
|
||||
packed_modules_mapping=config.get("packed_modules_mapping"),
|
||||
checkpoint_uses_packed_qkv=config.get("checkpoint_uses_packed_qkv", False),
|
||||
)
|
||||
|
||||
def is_layer_excluded(self, prefix: str):
|
||||
import regex as re
|
||||
|
||||
fused_patterns = ["q_a_proj", "q_b_proj", "kv_a_proj_with_mqa", "kv_b_proj"]
|
||||
prefix_split = prefix.split(".")
|
||||
for pattern in self.exclude_modules:
|
||||
regex_str = pattern.replace(".", r"\.").replace("*", r".*")
|
||||
pattern_split = pattern.split(".")
|
||||
if re.fullmatch(regex_str, prefix):
|
||||
return True
|
||||
elif (
|
||||
pattern_split[-1] in fused_patterns
|
||||
and pattern_split[-1] in prefix_split[-1]
|
||||
):
|
||||
assert len(prefix_split) == 5 and len(pattern_split) == 5
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_quant_method(self, layer: torch.nn.Module, prefix: str):
|
||||
return self._get_quant_method(layer, prefix, Linear=ModelOptFp4LinearMethod)
|
||||
|
||||
|
||||
class ModelOptFp8LinearMethod(LinearMethodBase):
|
||||
"""Linear method for ModelOpt static FP8 checkpoints."""
|
||||
|
||||
def __init__(self, quant_config: ModelOptFp8Config):
|
||||
self.quant_config = quant_config
|
||||
self.cutlass_fp8_supported = cutlass_fp8_supported()
|
||||
|
||||
def create_weights(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
input_size_per_partition: int,
|
||||
output_partition_sizes: List[int],
|
||||
input_size: int,
|
||||
output_size: int,
|
||||
params_dtype: torch.dtype,
|
||||
**extra_weight_attrs,
|
||||
):
|
||||
del input_size, output_size
|
||||
output_size_per_partition = sum(output_partition_sizes)
|
||||
weight_loader = extra_weight_attrs.get("weight_loader")
|
||||
|
||||
layer.logical_widths = output_partition_sizes
|
||||
layer.input_size_per_partition = input_size_per_partition
|
||||
layer.output_size_per_partition = output_size_per_partition
|
||||
|
||||
weight_dtype = (
|
||||
torch.float8_e4m3fn
|
||||
if self.quant_config.is_checkpoint_fp8_serialized
|
||||
else params_dtype
|
||||
)
|
||||
layer.register_parameter(
|
||||
"weight",
|
||||
ModelWeightParameter(
|
||||
data=torch.empty(
|
||||
output_size_per_partition,
|
||||
input_size_per_partition,
|
||||
dtype=weight_dtype,
|
||||
),
|
||||
input_dim=1,
|
||||
output_dim=0,
|
||||
weight_loader=weight_loader,
|
||||
),
|
||||
)
|
||||
|
||||
if self.quant_config.is_checkpoint_fp8_serialized:
|
||||
for scale_name in ["weight_scale", "input_scale"]:
|
||||
layer.register_parameter(
|
||||
scale_name,
|
||||
PerTensorScaleParameter(
|
||||
data=torch.full(
|
||||
(len(output_partition_sizes),),
|
||||
torch.finfo(torch.float32).min,
|
||||
dtype=torch.float32,
|
||||
),
|
||||
weight_loader=weight_loader,
|
||||
),
|
||||
)
|
||||
|
||||
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
|
||||
max_w_scale, quantized_weight = requantize_with_max_scale(
|
||||
layer.weight, layer.weight_scale, layer.logical_widths
|
||||
)
|
||||
# Preserve the parameter subclass metadata while rebinding to the
|
||||
# transposed FP8 view expected by the runtime.
|
||||
layer.weight.data = quantized_weight.t().detach()
|
||||
layer.weight.requires_grad_(False)
|
||||
if self.cutlass_fp8_supported:
|
||||
max_w_scale = convert_to_channelwise(max_w_scale, layer.logical_widths)
|
||||
copy_or_rebind_param(layer, "weight_scale", max_w_scale)
|
||||
copy_or_rebind_param(layer, "input_scale", layer.input_scale.max())
|
||||
|
||||
def apply(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
x: torch.Tensor,
|
||||
bias: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
return apply_fp8_linear(
|
||||
input=x,
|
||||
weight=layer.weight,
|
||||
weight_scale=layer.weight_scale,
|
||||
input_scale=layer.input_scale,
|
||||
bias=bias,
|
||||
cutlass_fp8_supported=self.cutlass_fp8_supported,
|
||||
)
|
||||
|
||||
|
||||
class ModelOptFp4LinearMethod(LinearMethodBase):
|
||||
"""NVFP4 linear method using CUTLASS FP4 GEMM."""
|
||||
|
||||
|
||||
@@ -161,6 +161,59 @@ class _Flux2Nvfp4FallbackAdapter(_TransformerQuantAdapter):
|
||||
)
|
||||
|
||||
|
||||
class _ModelOptFp8OffloadAdapter(_TransformerQuantAdapter):
|
||||
"""Adapter for diffusion ModelOpt FP8 checkpoints."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
server_args: ServerArgs,
|
||||
quant_config: Optional[QuantizationConfig],
|
||||
) -> None:
|
||||
self.server_args = server_args
|
||||
self.quant_config = quant_config
|
||||
|
||||
@staticmethod
|
||||
def _maybe_disable_incompatible_dit_offload_modes(
|
||||
server_args: ServerArgs,
|
||||
quant_config: Optional[QuantizationConfig],
|
||||
) -> None:
|
||||
if quant_config is None:
|
||||
return
|
||||
|
||||
quant_name_getter = getattr(type(quant_config), "get_name", None)
|
||||
quant_name = quant_name_getter() if callable(quant_name_getter) else None
|
||||
if quant_name != "modelopt_fp8":
|
||||
return
|
||||
|
||||
disabled_args: list[str] = []
|
||||
|
||||
if server_args.dit_cpu_offload:
|
||||
server_args.dit_cpu_offload = False
|
||||
disabled_args.append("dit_cpu_offload")
|
||||
|
||||
if server_args.dit_layerwise_offload:
|
||||
server_args.dit_layerwise_offload = False
|
||||
disabled_args.append("dit_layerwise_offload")
|
||||
|
||||
if not disabled_args:
|
||||
return
|
||||
|
||||
logger.warning(
|
||||
"ModelOpt FP8 diffusion checkpoints currently require the transformer "
|
||||
"FP8 weights to stay GPU-resident in their column-major layout; "
|
||||
"disabling %s for this run. Text encoder / VAE offload settings are "
|
||||
"left unchanged.",
|
||||
", ".join(disabled_args),
|
||||
)
|
||||
|
||||
def prepare(self) -> None:
|
||||
_ModelOptFp8OffloadAdapter._maybe_disable_incompatible_dit_offload_modes(
|
||||
server_args=self.server_args,
|
||||
quant_config=self.quant_config,
|
||||
)
|
||||
|
||||
|
||||
def resolve_transformer_safetensors_to_load(
|
||||
server_args: ServerArgs, component_model_path: str
|
||||
) -> list[str]:
|
||||
@@ -292,7 +345,11 @@ def _build_transformer_quant_adapters(
|
||||
cls_name=cls_name,
|
||||
server_args=server_args,
|
||||
quant_config=quant_config,
|
||||
)
|
||||
),
|
||||
_ModelOptFp8OffloadAdapter(
|
||||
server_args=server_args,
|
||||
quant_config=quant_config,
|
||||
),
|
||||
]
|
||||
if nunchaku_config is not None:
|
||||
adapters.append(
|
||||
@@ -305,6 +362,27 @@ def _build_transformer_quant_adapters(
|
||||
return adapters
|
||||
|
||||
|
||||
def _resolve_quant_config_from_transformer_override(
|
||||
transformer_weights_path: str,
|
||||
) -> Optional[QuantizationConfig]:
|
||||
"""Resolve quant config from an override transformer repo or directory."""
|
||||
override_quantized_path = maybe_download_model(transformer_weights_path)
|
||||
if not os.path.isdir(override_quantized_path):
|
||||
return None
|
||||
|
||||
override_config_path = os.path.join(override_quantized_path, "config.json")
|
||||
if not os.path.isfile(override_config_path):
|
||||
return None
|
||||
|
||||
with open(override_config_path, encoding="utf-8") as f:
|
||||
override_hf_config = json.load(f)
|
||||
|
||||
return get_quant_config(
|
||||
override_hf_config,
|
||||
override_quantized_path,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_quant_config(
|
||||
*,
|
||||
hf_config: dict,
|
||||
@@ -317,21 +395,29 @@ def _resolve_quant_config(
|
||||
priority: model config.json -> safetensors metadata -> format-specific fallback
|
||||
"""
|
||||
quant_config = get_quant_config(hf_config, component_model_path)
|
||||
if quant_config is None and server_args.transformer_weights_path:
|
||||
for safetensors_file in safetensors_list:
|
||||
quant_config = get_quant_config_from_safetensors_metadata(safetensors_file)
|
||||
if quant_config is not None:
|
||||
return quant_config
|
||||
if quant_config is not None or not server_args.transformer_weights_path:
|
||||
return quant_config
|
||||
|
||||
param_names_mapping_dict = (
|
||||
server_args.pipeline_config.dit_config.arch_config.param_names_mapping
|
||||
)
|
||||
quant_config = build_nvfp4_config_from_safetensors_list(
|
||||
safetensors_list, param_names_mapping_dict
|
||||
)
|
||||
quant_config = _resolve_quant_config_from_transformer_override(
|
||||
server_args.transformer_weights_path
|
||||
)
|
||||
if quant_config is not None:
|
||||
return quant_config
|
||||
|
||||
for safetensors_file in safetensors_list:
|
||||
quant_config = get_quant_config_from_safetensors_metadata(safetensors_file)
|
||||
if quant_config is not None:
|
||||
return quant_config
|
||||
|
||||
param_names_mapping_dict = (
|
||||
server_args.pipeline_config.dit_config.arch_config.param_names_mapping
|
||||
)
|
||||
quant_config = build_nvfp4_config_from_safetensors_list(
|
||||
safetensors_list, param_names_mapping_dict
|
||||
)
|
||||
if quant_config is not None:
|
||||
return quant_config
|
||||
|
||||
return quant_config
|
||||
|
||||
|
||||
|
||||
@@ -172,9 +172,12 @@ class Flux2Attention(torch.nn.Module, AttentionModuleMixin):
|
||||
self.added_kv_proj_dim = added_kv_proj_dim
|
||||
self.added_proj_bias = added_proj_bias
|
||||
|
||||
# Fuse Q/K/V into a single linear when using NVFP4: the checkpoint stores them
|
||||
# packed as one tensor, so a fused layer avoids splitting during weight loading.
|
||||
self.use_fused_qkv = isinstance(quant_config, ModelOptFp4Config)
|
||||
# Some FLUX.2 NVFP4 checkpoints store Q/K/V packed as a single tensor, while
|
||||
# ModelOpt's standard diffusers export keeps the original to_q/to_k/to_v layout.
|
||||
# Only enable the fused loader path for the packed checkpoint family.
|
||||
self.use_fused_qkv = isinstance(quant_config, ModelOptFp4Config) and getattr(
|
||||
quant_config, "checkpoint_uses_packed_qkv", False
|
||||
)
|
||||
self.use_fused_added_qkv = self.use_fused_qkv
|
||||
|
||||
if self.use_fused_qkv:
|
||||
@@ -484,7 +487,10 @@ class Flux2ParallelSelfAttention(torch.nn.Module, AttentionModuleMixin):
|
||||
param.data.copy_(loaded_weight)
|
||||
|
||||
self.to_out.weight_loader = _loader
|
||||
self.to_out.weight.weight_loader = _loader
|
||||
if hasattr(self.to_out.weight, "_weight_loader"):
|
||||
self.to_out.weight._weight_loader = _loader
|
||||
else:
|
||||
self.to_out.weight.weight_loader = _loader
|
||||
|
||||
def forward(
|
||||
self,
|
||||
@@ -827,7 +833,11 @@ class Flux2PosEmbed(nn.Module):
|
||||
repeat_interleave_real=False,
|
||||
dtype=(
|
||||
torch.float64
|
||||
if current_platform.is_float64_supported()
|
||||
if (
|
||||
current_platform.is_float64_supported()
|
||||
if hasattr(current_platform, "is_float64_supported")
|
||||
else True
|
||||
)
|
||||
else torch.float32
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import torch
|
||||
from safetensors import safe_open
|
||||
|
||||
from sglang.multimodal_gen.runtime.layers.quantization import (
|
||||
@@ -15,6 +17,34 @@ from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def _resolve_quant_method_name(quant_cfg: dict) -> str:
|
||||
quant_method = quant_cfg.get("quant_method")
|
||||
if quant_method != "modelopt":
|
||||
return quant_method
|
||||
|
||||
quant_algo = (
|
||||
quant_cfg.get("quant_algo")
|
||||
or quant_cfg.get("quantization", {}).get("quant_algo")
|
||||
or ""
|
||||
).upper()
|
||||
if quant_algo == "MIXED_PRECISION":
|
||||
raise ValueError(
|
||||
"ModelOpt mixed precision is not supported by the current SGLang diffusion runtime."
|
||||
)
|
||||
if "FP8" in quant_algo:
|
||||
return "modelopt_fp8"
|
||||
if "FP4" in quant_algo or "NVFP4" in quant_algo:
|
||||
return "modelopt_fp4"
|
||||
raise ValueError(f"Unsupported ModelOpt quant_algo for diffusion: {quant_algo}")
|
||||
|
||||
|
||||
def _load_quant_cls(quant_cfg: dict):
|
||||
quant_method = _resolve_quant_method_name(quant_cfg)
|
||||
if not quant_method:
|
||||
raise ValueError("Missing quant_method in quantization config.")
|
||||
return get_quantization_config(quant_method)
|
||||
|
||||
|
||||
def find_quant_modelslim_config(model_config, component_model_path):
|
||||
quant_config_file = Path(component_model_path, "quant_model_description.json")
|
||||
quant_cfg = None
|
||||
@@ -41,74 +71,70 @@ def get_quant_config(
|
||||
packed_modules_mapping: Dict[str, List[str]] = {},
|
||||
remap_prefix: Dict[str, str] | None = None,
|
||||
) -> QuantizationConfig:
|
||||
|
||||
quant_cfg = find_quant_modelslim_config(model_config, component_model_path)
|
||||
if quant_cfg is not None:
|
||||
quant_cls = get_quantization_config(quant_cfg["quant_method"])
|
||||
quant_cls = _load_quant_cls(quant_cfg)
|
||||
return quant_cls.from_config(quant_cfg)
|
||||
else:
|
||||
if "quantization_config" not in model_config:
|
||||
return None
|
||||
quant_cls = get_quantization_config(
|
||||
model_config["quantization_config"]["quant_method"]
|
||||
|
||||
if "quantization_config" not in model_config:
|
||||
return None
|
||||
|
||||
hf_quant_config = model_config["quantization_config"]
|
||||
if hf_quant_config is not None and not isinstance(hf_quant_config, dict):
|
||||
hf_quant_config = hf_quant_config.to_dict()
|
||||
quant_cls = _load_quant_cls(hf_quant_config)
|
||||
|
||||
# GGUF doesn't have config file
|
||||
if hf_quant_config["quant_method"] == "gguf":
|
||||
return quant_cls.from_config({})
|
||||
|
||||
# some vision model may keep quantization_config in their text_config
|
||||
hf_text_config = getattr(model_config, "text_config", None)
|
||||
if hf_quant_config is None and hf_text_config is not None:
|
||||
hf_quant_config = getattr(hf_text_config, "quantization_config", None)
|
||||
if hf_quant_config is None:
|
||||
# compressed-tensors uses a compressions_config
|
||||
hf_quant_config = getattr(model_config, "compression_config", None)
|
||||
if hf_quant_config is not None:
|
||||
hf_quant_config["packed_modules_mapping"] = packed_modules_mapping
|
||||
return quant_cls.from_config(hf_quant_config)
|
||||
|
||||
model_name_or_path = model_config["model_path"]
|
||||
is_local = os.path.isdir(model_name_or_path)
|
||||
hf_folder = model_name_or_path
|
||||
|
||||
possible_config_filenames = quant_cls.get_config_filenames()
|
||||
|
||||
# If the quantization config is not found, use the default config.
|
||||
if not possible_config_filenames:
|
||||
return quant_cls()
|
||||
|
||||
config_files = glob.glob(os.path.join(hf_folder, "*.json"))
|
||||
|
||||
quant_config_files = [
|
||||
f for f in config_files if any(f.endswith(x) for x in possible_config_filenames)
|
||||
]
|
||||
if len(quant_config_files) == 0:
|
||||
raise ValueError(
|
||||
f"Cannot find the config file for {model_config['quantization_config']['quant_method']}"
|
||||
)
|
||||
if len(quant_config_files) > 1:
|
||||
raise ValueError(
|
||||
f"Found multiple config files for {model_config['quantization_config']['quant_method']}: "
|
||||
f"{quant_config_files}"
|
||||
)
|
||||
|
||||
# GGUF doesn't have config file
|
||||
if model_config["quantization_config"]["quant_method"] == "gguf":
|
||||
return quant_cls.from_config({})
|
||||
|
||||
# Read the quantization config from the HF model config, if available.
|
||||
hf_quant_config = model_config["quantization_config"]
|
||||
# some vision model may keep quantization_config in their text_config
|
||||
hf_text_config = getattr(model_config, "text_config", None)
|
||||
if hf_quant_config is None and hf_text_config is not None:
|
||||
hf_quant_config = getattr(hf_text_config, "quantization_config", None)
|
||||
if hf_quant_config is None:
|
||||
# compressed-tensors uses a compressions_config
|
||||
hf_quant_config = getattr(model_config, "compression_config", None)
|
||||
if hf_quant_config is not None:
|
||||
hf_quant_config["packed_modules_mapping"] = packed_modules_mapping
|
||||
return quant_cls.from_config(hf_quant_config)
|
||||
# In case of bitsandbytes/QLoRA, get quant config from the adapter model.
|
||||
else:
|
||||
model_name_or_path = model_config["model_path"]
|
||||
is_local = os.path.isdir(model_name_or_path)
|
||||
hf_folder = model_name_or_path
|
||||
|
||||
possible_config_filenames = quant_cls.get_config_filenames()
|
||||
|
||||
# If the quantization config is not found, use the default config.
|
||||
if not possible_config_filenames:
|
||||
return quant_cls()
|
||||
|
||||
config_files = glob.glob(os.path.join(hf_folder, "*.json"))
|
||||
|
||||
quant_config_files = [
|
||||
f
|
||||
for f in config_files
|
||||
if any(f.endswith(x) for x in possible_config_filenames)
|
||||
]
|
||||
if len(quant_config_files) == 0:
|
||||
raise ValueError(
|
||||
f"Cannot find the config file for {model_config['quantization_config']['quant_method']}"
|
||||
)
|
||||
if len(quant_config_files) > 1:
|
||||
raise ValueError(
|
||||
f"Found multiple config files for {model_config['quantization_config']['quant_method']}: "
|
||||
f"{quant_config_files}"
|
||||
)
|
||||
|
||||
quant_config_file = quant_config_files[0]
|
||||
with open(quant_config_file) as f:
|
||||
config = json.load(f)
|
||||
if remap_prefix is not None:
|
||||
exclude_modules = [
|
||||
replace_prefix(key, remap_prefix)
|
||||
for key in config["quantization"]["exclude_modules"]
|
||||
]
|
||||
config["quantization"]["exclude_modules"] = exclude_modules
|
||||
config["packed_modules_mapping"] = packed_modules_mapping
|
||||
return quant_cls.from_config(config)
|
||||
quant_config_file = quant_config_files[0]
|
||||
with open(quant_config_file) as f:
|
||||
config = json.load(f)
|
||||
if remap_prefix is not None and "quantization" in config:
|
||||
exclude_modules = [
|
||||
replace_prefix(key, remap_prefix)
|
||||
for key in config["quantization"]["exclude_modules"]
|
||||
]
|
||||
config["quantization"]["exclude_modules"] = exclude_modules
|
||||
config["packed_modules_mapping"] = packed_modules_mapping
|
||||
return quant_cls.from_config(config)
|
||||
|
||||
|
||||
def handle_fp8_metadata_format(quant_config_dict):
|
||||
@@ -132,11 +158,23 @@ def get_quant_config_from_safetensors_metadata(
|
||||
return None
|
||||
|
||||
quant_config_str = metadata.get("_quantization_metadata")
|
||||
if not quant_config_str:
|
||||
return None
|
||||
try:
|
||||
quant_config_dict = json.loads(quant_config_str)
|
||||
except Exception as _e:
|
||||
quant_config_dict = None
|
||||
if quant_config_str:
|
||||
try:
|
||||
quant_config_dict = json.loads(quant_config_str)
|
||||
except Exception:
|
||||
quant_config_dict = None
|
||||
|
||||
if quant_config_dict is None:
|
||||
quant_config_str = metadata.get("quantization_config")
|
||||
if not quant_config_str:
|
||||
return None
|
||||
try:
|
||||
quant_config_dict = json.loads(quant_config_str)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if not quant_config_dict:
|
||||
return None
|
||||
|
||||
# handle diffusers fp8 safetensors metadata format
|
||||
@@ -152,7 +190,7 @@ def get_quant_config_from_safetensors_metadata(
|
||||
return None
|
||||
|
||||
try:
|
||||
quant_cls = get_quantization_config(quant_method)
|
||||
quant_cls = _load_quant_cls(quant_config_dict)
|
||||
config = quant_cls.from_config(quant_config_dict)
|
||||
logger.debug(f"Get quantization config from safetensors file: {file_path}")
|
||||
return config
|
||||
@@ -179,12 +217,14 @@ def _build_nvfp4_config_from_safetensors_files(
|
||||
safetensors. Building the config from only the first matching file can
|
||||
incorrectly exclude layers that are quantized in a later shard.
|
||||
"""
|
||||
import torch
|
||||
|
||||
group_size = None
|
||||
quantized_bfl_modules: set[str] = set()
|
||||
non_quantized_bfl_modules: set[str] = set()
|
||||
files_with_nvfp4_metadata: list[str] = []
|
||||
checkpoint_uses_packed_qkv = False
|
||||
packed_qkv_pattern = re.compile(
|
||||
r"^(double_blocks\.\d+\.(img|txt)_attn\.qkv|single_blocks\.\d+\.linear1)\."
|
||||
)
|
||||
|
||||
for file_path in file_paths:
|
||||
metadata = get_metadata_from_safetensors_file(file_path)
|
||||
@@ -216,6 +256,8 @@ def _build_nvfp4_config_from_safetensors_files(
|
||||
|
||||
with safe_open(file_path, framework="pt", device="cpu") as f:
|
||||
all_keys = set(f.keys())
|
||||
if any(packed_qkv_pattern.match(k) for k in all_keys):
|
||||
checkpoint_uses_packed_qkv = True
|
||||
|
||||
if group_size is None:
|
||||
for layer_name in file_quantized_modules:
|
||||
@@ -263,13 +305,19 @@ def _build_nvfp4_config_from_safetensors_files(
|
||||
try:
|
||||
quant_cls = get_quantization_config("modelopt_fp4")
|
||||
result = quant_cls.from_config(
|
||||
{"quant_algo": "NVFP4", "group_size": group_size, "ignore": exclude_modules}
|
||||
{
|
||||
"quant_algo": "NVFP4",
|
||||
"group_size": group_size,
|
||||
"ignore": exclude_modules,
|
||||
"checkpoint_uses_packed_qkv": checkpoint_uses_packed_qkv,
|
||||
}
|
||||
)
|
||||
logger.info(
|
||||
"Built NVFP4 quant config from %d safetensors: group_size=%d, %d excluded modules",
|
||||
"Built NVFP4 quant config from %d safetensors: group_size=%d, %d excluded modules, packed_qkv=%s",
|
||||
len(files_with_nvfp4_metadata),
|
||||
group_size,
|
||||
len(exclude_modules),
|
||||
checkpoint_uses_packed_qkv,
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,498 @@
|
||||
"""Compare diffusion BF16 and quantized runs via trajectory-latent similarity.
|
||||
|
||||
This tool runs two SGLang diffusion variants with the same prompt and seed,
|
||||
captures intermediate denoising latents via `return_trajectory_latents`, and
|
||||
reports cosine / error metrics for each timestep plus final frame metrics.
|
||||
|
||||
The intended use is quant validation on reduced deterministic smoke settings:
|
||||
- same prompt / seed / resolution / step count for both variants
|
||||
- BF16 reference on the base model
|
||||
- FP8 candidate via `--candidate-transformer-path` and/or component overrides
|
||||
|
||||
Example:
|
||||
|
||||
python -m sglang.multimodal_gen.tools.compare_diffusion_trajectory_similarity \
|
||||
--model-path /path/to/model \
|
||||
--prompt "A futuristic cyberpunk city at night" \
|
||||
--width 512 --height 512 --num-inference-steps 8 --seed 42 \
|
||||
--text-encoder-cpu-offload \
|
||||
--candidate-transformer-path /tmp/modelopt_flux2_fp8/sglang_transformer \
|
||||
--output-json /tmp/flux2_similarity.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Any, Sequence
|
||||
|
||||
import imageio.v3 as iio
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
def parse_component_overrides(entries: Sequence[str] | None) -> dict[str, str]:
|
||||
overrides: dict[str, str] = {}
|
||||
for entry in entries or []:
|
||||
if "=" not in entry:
|
||||
raise ValueError(
|
||||
f"Invalid component override '{entry}'. Expected format component=path."
|
||||
)
|
||||
component, path = entry.split("=", 1)
|
||||
component = component.strip().replace("-", "_")
|
||||
path = path.strip()
|
||||
if not component or not path:
|
||||
raise ValueError(
|
||||
f"Invalid component override '{entry}'. Expected format component=path."
|
||||
)
|
||||
overrides[component] = path
|
||||
return overrides
|
||||
|
||||
|
||||
def _cosine_similarity(flat_a: torch.Tensor, flat_b: torch.Tensor) -> float:
|
||||
norm_a = torch.linalg.vector_norm(flat_a).item()
|
||||
norm_b = torch.linalg.vector_norm(flat_b).item()
|
||||
if norm_a == 0.0 and norm_b == 0.0:
|
||||
return 1.0
|
||||
if norm_a == 0.0 or norm_b == 0.0:
|
||||
return 0.0
|
||||
return float(F.cosine_similarity(flat_a, flat_b, dim=0).item())
|
||||
|
||||
|
||||
def compute_tensor_metrics(lhs: Any, rhs: Any) -> dict[str, float]:
|
||||
lhs_tensor = torch.as_tensor(lhs).detach().cpu().float()
|
||||
rhs_tensor = torch.as_tensor(rhs).detach().cpu().float()
|
||||
if lhs_tensor.shape != rhs_tensor.shape:
|
||||
raise ValueError(
|
||||
f"Metric shape mismatch: {tuple(lhs_tensor.shape)} vs {tuple(rhs_tensor.shape)}"
|
||||
)
|
||||
|
||||
diff = lhs_tensor - rhs_tensor
|
||||
mse = float(diff.square().mean().item())
|
||||
rmse = float(math.sqrt(mse))
|
||||
mae = float(diff.abs().mean().item())
|
||||
max_abs = float(diff.abs().max().item())
|
||||
l2 = float(torch.linalg.vector_norm(diff).item())
|
||||
cosine = _cosine_similarity(lhs_tensor.reshape(-1), rhs_tensor.reshape(-1))
|
||||
return {
|
||||
"cosine_similarity": cosine,
|
||||
"mae": mae,
|
||||
"mse": mse,
|
||||
"rmse": rmse,
|
||||
"max_abs": max_abs,
|
||||
"l2": l2,
|
||||
}
|
||||
|
||||
|
||||
def compute_uint8_frame_metrics(lhs: Any, rhs: Any) -> dict[str, float]:
|
||||
metrics = compute_tensor_metrics(lhs, rhs)
|
||||
mse = metrics["mse"]
|
||||
metrics["psnr_db"] = (
|
||||
float("inf") if mse == 0.0 else 20 * math.log10(255.0) - 10 * math.log10(mse)
|
||||
)
|
||||
return metrics
|
||||
|
||||
|
||||
def _normalize_step_index(step_index: int, num_steps: int) -> int:
|
||||
if num_steps <= 0:
|
||||
raise ValueError("num_steps must be positive.")
|
||||
if step_index < 0:
|
||||
step_index += num_steps
|
||||
if step_index < 0 or step_index >= num_steps:
|
||||
raise IndexError(
|
||||
f"Requested step index {step_index} is outside the valid range [0, {num_steps})."
|
||||
)
|
||||
return step_index
|
||||
|
||||
|
||||
def _maybe_scalar(timestep: torch.Tensor | None, index: int) -> float | None:
|
||||
if timestep is None:
|
||||
return None
|
||||
value = timestep[index]
|
||||
if isinstance(value, torch.Tensor):
|
||||
value = value.detach().cpu()
|
||||
if value.numel() == 1:
|
||||
return float(value.item())
|
||||
return float(value)
|
||||
|
||||
|
||||
def summarize_trajectory_metrics(
|
||||
reference_latents: Any,
|
||||
candidate_latents: Any,
|
||||
*,
|
||||
reference_timesteps: Any = None,
|
||||
candidate_timesteps: Any = None,
|
||||
step_index: int = -1,
|
||||
) -> dict[str, Any]:
|
||||
ref = torch.as_tensor(reference_latents).detach().cpu().float()
|
||||
cand = torch.as_tensor(candidate_latents).detach().cpu().float()
|
||||
if ref.shape != cand.shape:
|
||||
raise ValueError(
|
||||
f"Trajectory shape mismatch: {tuple(ref.shape)} vs {tuple(cand.shape)}"
|
||||
)
|
||||
if ref.ndim < 2:
|
||||
raise ValueError(
|
||||
f"Expected trajectory latents with an explicit timestep dimension, got {tuple(ref.shape)}"
|
||||
)
|
||||
|
||||
num_steps = ref.shape[1]
|
||||
selected_step = _normalize_step_index(step_index, num_steps)
|
||||
ref_t = (
|
||||
torch.as_tensor(reference_timesteps).detach().cpu()
|
||||
if reference_timesteps is not None
|
||||
else None
|
||||
)
|
||||
cand_t = (
|
||||
torch.as_tensor(candidate_timesteps).detach().cpu()
|
||||
if candidate_timesteps is not None
|
||||
else None
|
||||
)
|
||||
|
||||
per_step: list[dict[str, Any]] = []
|
||||
for idx in range(num_steps):
|
||||
metrics = compute_tensor_metrics(ref[:, idx], cand[:, idx])
|
||||
metrics["step_index"] = idx
|
||||
metrics["reference_timestep"] = _maybe_scalar(ref_t, idx)
|
||||
metrics["candidate_timestep"] = _maybe_scalar(cand_t, idx)
|
||||
per_step.append(metrics)
|
||||
|
||||
return {
|
||||
"trajectory_shape": list(ref.shape),
|
||||
"num_steps": num_steps,
|
||||
"selected_step_index": selected_step,
|
||||
"selected_step_metrics": per_step[selected_step],
|
||||
"per_step_metrics": per_step,
|
||||
}
|
||||
|
||||
|
||||
def summarize_output_frame_metrics(
|
||||
reference_frames: Sequence[Any],
|
||||
candidate_frames: Sequence[Any],
|
||||
) -> dict[str, Any]:
|
||||
if len(reference_frames) != len(candidate_frames):
|
||||
raise ValueError(
|
||||
f"Output frame count mismatch: {len(reference_frames)} vs {len(candidate_frames)}"
|
||||
)
|
||||
if not reference_frames:
|
||||
raise ValueError("No output frames available for comparison.")
|
||||
|
||||
ref_stack = np.stack([np.asarray(frame) for frame in reference_frames], axis=0)
|
||||
cand_stack = np.stack([np.asarray(frame) for frame in candidate_frames], axis=0)
|
||||
|
||||
frame0_metrics = compute_uint8_frame_metrics(ref_stack[0], cand_stack[0])
|
||||
mid_index = len(reference_frames) // 2
|
||||
mid_metrics = compute_uint8_frame_metrics(
|
||||
ref_stack[mid_index], cand_stack[mid_index]
|
||||
)
|
||||
all_metrics = compute_uint8_frame_metrics(ref_stack, cand_stack)
|
||||
|
||||
return {
|
||||
"num_frames": len(reference_frames),
|
||||
"frame0_metrics": frame0_metrics,
|
||||
"mid_frame_index": mid_index,
|
||||
"mid_frame_metrics": mid_metrics,
|
||||
"all_frames_metrics": all_metrics,
|
||||
}
|
||||
|
||||
|
||||
def extract_result_frames(result: Any) -> list[np.ndarray]:
|
||||
if result.frames is not None:
|
||||
return [np.asarray(frame) for frame in result.frames]
|
||||
|
||||
sample = result.samples
|
||||
if sample is None:
|
||||
if result.output_file_path:
|
||||
output_path = Path(result.output_file_path)
|
||||
if not output_path.exists():
|
||||
raise ValueError(
|
||||
"GenerationResult did not contain frames or samples, and its "
|
||||
f"output_file_path does not exist: {output_path}"
|
||||
)
|
||||
if output_path.suffix.lower() in {".png", ".jpg", ".jpeg", ".webp"}:
|
||||
return [np.asarray(iio.imread(output_path))]
|
||||
return [np.asarray(frame) for frame in iio.imiter(output_path)]
|
||||
raise ValueError(
|
||||
"GenerationResult did not contain frames, samples, or a readable output_file_path."
|
||||
)
|
||||
|
||||
if isinstance(sample, torch.Tensor):
|
||||
tensor = sample.detach().cpu().float()
|
||||
if tensor.ndim == 3:
|
||||
tensor = tensor.unsqueeze(1)
|
||||
if tensor.ndim != 4:
|
||||
raise ValueError(
|
||||
f"Unsupported tensor sample shape for frame extraction: {tuple(tensor.shape)}"
|
||||
)
|
||||
tensor = (tensor * 255).clamp(0, 255).to(torch.uint8)
|
||||
frames = tensor.permute(1, 2, 3, 0).contiguous().numpy()
|
||||
return [frame for frame in frames]
|
||||
|
||||
array = np.asarray(sample)
|
||||
if array.ndim == 2:
|
||||
array = array[..., None]
|
||||
if array.ndim == 3:
|
||||
if array.shape[-1] in (1, 3, 4):
|
||||
array = array[None, ...]
|
||||
else:
|
||||
array = array[..., None]
|
||||
if array.ndim != 4:
|
||||
raise ValueError(
|
||||
f"Unsupported numpy sample shape for frame extraction: {tuple(array.shape)}"
|
||||
)
|
||||
if array.dtype != np.uint8:
|
||||
array = (np.clip(array, 0.0, 1.0) * 255.0).astype(np.uint8)
|
||||
return [frame for frame in array]
|
||||
|
||||
|
||||
def build_server_kwargs(args: argparse.Namespace, *, variant: str) -> dict[str, Any]:
|
||||
component_paths = parse_component_overrides(
|
||||
getattr(args, f"{variant}_component_path") or []
|
||||
)
|
||||
transformer_path = getattr(args, f"{variant}_transformer_path")
|
||||
|
||||
kwargs: dict[str, Any] = {
|
||||
"model_path": args.model_path,
|
||||
"model_id": args.model_id,
|
||||
"backend": args.backend,
|
||||
"num_gpus": args.num_gpus,
|
||||
"dit_cpu_offload": args.dit_cpu_offload,
|
||||
"dit_layerwise_offload": args.dit_layerwise_offload,
|
||||
"text_encoder_cpu_offload": args.text_encoder_cpu_offload,
|
||||
"vae_cpu_offload": args.vae_cpu_offload,
|
||||
"pin_cpu_memory": args.pin_cpu_memory,
|
||||
"enable_cfg_parallel": args.enable_cfg_parallel,
|
||||
"ulysses_degree": args.ulysses_degree,
|
||||
}
|
||||
if args.sp_degree is not None:
|
||||
kwargs["sp_degree"] = args.sp_degree
|
||||
if transformer_path is not None:
|
||||
kwargs["transformer_weights_path"] = transformer_path
|
||||
if component_paths:
|
||||
kwargs["component_paths"] = component_paths
|
||||
return kwargs
|
||||
|
||||
|
||||
def build_sampling_kwargs(
|
||||
args: argparse.Namespace, *, output_dir: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
kwargs: dict[str, Any] = {
|
||||
"prompt": args.prompt,
|
||||
"width": args.width,
|
||||
"height": args.height,
|
||||
"num_inference_steps": args.num_inference_steps,
|
||||
"guidance_scale": args.guidance_scale,
|
||||
"seed": args.seed,
|
||||
"return_frames": True,
|
||||
"return_trajectory_latents": True,
|
||||
"return_trajectory_decoded": args.return_trajectory_decoded,
|
||||
"save_output": output_dir is not None,
|
||||
}
|
||||
if output_dir is not None:
|
||||
kwargs["output_path"] = output_dir
|
||||
if args.num_frames is not None:
|
||||
kwargs["num_frames"] = args.num_frames
|
||||
if args.guidance_scale_2 is not None:
|
||||
kwargs["guidance_scale_2"] = args.guidance_scale_2
|
||||
return kwargs
|
||||
|
||||
|
||||
def run_variant(
|
||||
*,
|
||||
server_kwargs: dict[str, Any],
|
||||
sampling_kwargs: dict[str, Any],
|
||||
):
|
||||
from sglang.multimodal_gen.runtime.entrypoints.diffusion_generator import (
|
||||
DiffGenerator,
|
||||
)
|
||||
|
||||
with DiffGenerator.from_pretrained(local_mode=True, **server_kwargs) as generator:
|
||||
result = generator.generate(sampling_params_kwargs=sampling_kwargs)
|
||||
|
||||
if isinstance(result, list):
|
||||
if len(result) != 1:
|
||||
raise ValueError(
|
||||
f"Expected a single generation result, got {len(result)} results."
|
||||
)
|
||||
result = result[0]
|
||||
if result is None:
|
||||
raise RuntimeError("Generation returned no result.")
|
||||
return result
|
||||
|
||||
|
||||
def _to_jsonable(result: dict[str, Any]) -> dict[str, Any]:
|
||||
return json.loads(json.dumps(result, allow_nan=True))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--model-path", required=True)
|
||||
parser.add_argument(
|
||||
"--model-id",
|
||||
help=(
|
||||
"Optional model ID override passed to DiffGenerator.from_pretrained. "
|
||||
"Use this when --model-path points to a local directory whose name "
|
||||
"does not match a registered native SGLang model."
|
||||
),
|
||||
)
|
||||
parser.add_argument("--backend", default="sglang")
|
||||
parser.add_argument("--prompt", required=True)
|
||||
parser.add_argument("--output-json", required=True)
|
||||
parser.add_argument("--width", type=int, required=True)
|
||||
parser.add_argument("--height", type=int, required=True)
|
||||
parser.add_argument("--num-frames", type=int)
|
||||
parser.add_argument("--num-inference-steps", type=int, required=True)
|
||||
parser.add_argument("--guidance-scale", type=float, required=True)
|
||||
parser.add_argument("--guidance-scale-2", type=float)
|
||||
parser.add_argument("--seed", type=int, default=42)
|
||||
parser.add_argument("--num-gpus", type=int, default=1)
|
||||
parser.add_argument("--ulysses-degree", type=int, default=1)
|
||||
parser.add_argument("--sp-degree", type=int)
|
||||
parser.add_argument("--trajectory-step-index", type=int, default=-1)
|
||||
parser.add_argument("--reference-transformer-path")
|
||||
parser.add_argument("--candidate-transformer-path")
|
||||
parser.add_argument(
|
||||
"--reference-component-path",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Repeatable component override in the form component=path.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--candidate-component-path",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Repeatable component override in the form component=path.",
|
||||
)
|
||||
parser.add_argument("--save-output-dir")
|
||||
parser.add_argument(
|
||||
"--return-trajectory-decoded",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enable-cfg-parallel",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--text-encoder-cpu-offload",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--vae-cpu-offload",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dit-cpu-offload",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dit-layerwise-offload",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pin-cpu-memory",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
output_json = Path(args.output_json).expanduser().resolve()
|
||||
output_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
save_root: Path | None = None
|
||||
if args.save_output_dir:
|
||||
save_root = Path(args.save_output_dir).expanduser().resolve()
|
||||
save_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ref_server_kwargs = build_server_kwargs(args, variant="reference")
|
||||
cand_server_kwargs = build_server_kwargs(args, variant="candidate")
|
||||
|
||||
ref_sampling_kwargs = build_sampling_kwargs(
|
||||
args,
|
||||
output_dir=str(save_root / "reference") if save_root else None,
|
||||
)
|
||||
cand_sampling_kwargs = build_sampling_kwargs(
|
||||
args,
|
||||
output_dir=str(save_root / "candidate") if save_root else None,
|
||||
)
|
||||
|
||||
reference = run_variant(
|
||||
server_kwargs=ref_server_kwargs,
|
||||
sampling_kwargs=ref_sampling_kwargs,
|
||||
)
|
||||
candidate = run_variant(
|
||||
server_kwargs=cand_server_kwargs,
|
||||
sampling_kwargs=cand_sampling_kwargs,
|
||||
)
|
||||
|
||||
result = {
|
||||
"model_path": args.model_path,
|
||||
"prompt": args.prompt,
|
||||
"seed": args.seed,
|
||||
"server_kwargs": {
|
||||
"reference": ref_server_kwargs,
|
||||
"candidate": cand_server_kwargs,
|
||||
},
|
||||
"sampling_kwargs": {
|
||||
"width": args.width,
|
||||
"height": args.height,
|
||||
"num_frames": args.num_frames,
|
||||
"num_inference_steps": args.num_inference_steps,
|
||||
"guidance_scale": args.guidance_scale,
|
||||
"guidance_scale_2": args.guidance_scale_2,
|
||||
},
|
||||
"reference_generation": {
|
||||
"generation_time_s": reference.generation_time,
|
||||
"peak_memory_mb": reference.peak_memory_mb,
|
||||
"output_file_path": reference.output_file_path,
|
||||
},
|
||||
"candidate_generation": {
|
||||
"generation_time_s": candidate.generation_time,
|
||||
"peak_memory_mb": candidate.peak_memory_mb,
|
||||
"output_file_path": candidate.output_file_path,
|
||||
},
|
||||
"trajectory_metrics": summarize_trajectory_metrics(
|
||||
reference.trajectory_latents,
|
||||
candidate.trajectory_latents,
|
||||
reference_timesteps=reference.trajectory_timesteps,
|
||||
candidate_timesteps=candidate.trajectory_timesteps,
|
||||
step_index=args.trajectory_step_index,
|
||||
),
|
||||
"output_metrics": summarize_output_frame_metrics(
|
||||
extract_result_frames(reference),
|
||||
extract_result_frames(candidate),
|
||||
),
|
||||
}
|
||||
|
||||
output_json.write_text(
|
||||
json.dumps(_to_jsonable(result), indent=2, sort_keys=True), encoding="utf-8"
|
||||
)
|
||||
|
||||
selected = result["trajectory_metrics"]["selected_step_metrics"]
|
||||
frame0 = result["output_metrics"]["frame0_metrics"]
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"output_json": str(output_json),
|
||||
"trajectory_selected_step": result["trajectory_metrics"][
|
||||
"selected_step_index"
|
||||
],
|
||||
"trajectory_cosine": selected["cosine_similarity"],
|
||||
"trajectory_mae": selected["mae"],
|
||||
"frame0_psnr_db": frame0["psnr_db"],
|
||||
"frame0_mae": frame0["mae"],
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,494 @@
|
||||
"""Convert a ModelOpt diffusion FP8 export into an SGLang-loadable checkpoint.
|
||||
|
||||
The core conversion path is model-agnostic:
|
||||
- read the ModelOpt diffusers transformer export
|
||||
- rebuild per-layer `weight_scale` / `input_scale` tensors from `backbone.pt`
|
||||
- materialize SGLang-native `float8_e4m3fn` weights
|
||||
- preserve ModelOpt `ignore` layers in their original dtype
|
||||
|
||||
Some models still benefit from a small validated BF16 fallback set. Those
|
||||
fallback profiles are intentionally isolated so the generic FP8 conversion path
|
||||
remains reusable across future diffusion backbones.
|
||||
|
||||
Example:
|
||||
|
||||
python -m sglang.multimodal_gen.tools.convert_modelopt_fp8_checkpoint \
|
||||
--modelopt-hf-dir /tmp/modelopt_flux2_fp8/hf \
|
||||
--modelopt-backbone-ckpt /tmp/modelopt_flux2_fp8/ckpt/backbone.pt \
|
||||
--base-transformer-dir /path/to/FLUX.2-dev/transformer \
|
||||
--output-dir /tmp/modelopt_flux2_fp8/sglang_transformer
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gc
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Mapping, Sequence
|
||||
|
||||
import torch
|
||||
from safetensors import safe_open
|
||||
from safetensors.torch import load_file, save_file
|
||||
|
||||
INDEX_FILENAMES = [
|
||||
"model.safetensors.index.json",
|
||||
"diffusion_pytorch_model.safetensors.index.json",
|
||||
]
|
||||
FP8_E4M3_MAXBOUND = 448.0
|
||||
DEFAULT_FLUX2_KEEP_BF16_PATTERNS = [
|
||||
r"^time_guidance_embed\.(timestep_embedder|guidance_embedder)\.linear_[12]$",
|
||||
r"^double_stream_modulation_(img|txt)\.linear$",
|
||||
r"^single_stream_modulation\.linear$",
|
||||
r"^x_embedder$",
|
||||
r"^context_embedder$",
|
||||
r"^norm_out\.linear$",
|
||||
]
|
||||
DEFAULT_FLUX1_KEEP_BF16_PATTERNS = [
|
||||
r"^transformer_blocks\.\d+\.norm1\.linear$",
|
||||
r"^transformer_blocks\.\d+\.norm1_context\.linear$",
|
||||
r"^transformer_blocks\.\d+\.ff\.net\.0\.proj$",
|
||||
r"^transformer_blocks\.\d+\.ff\.net\.2$",
|
||||
r"^transformer_blocks\.\d+\.ff_context\.net\.0\.proj$",
|
||||
r"^transformer_blocks\.\d+\.ff_context\.net\.2$",
|
||||
r"^single_transformer_blocks\.\d+\.norm\.linear$",
|
||||
]
|
||||
|
||||
|
||||
def _resolve_transformer_dir(path: str) -> str:
|
||||
candidate = Path(path).expanduser().resolve()
|
||||
if (candidate / "config.json").is_file():
|
||||
return str(candidate)
|
||||
transformer_dir = candidate / "transformer"
|
||||
if (transformer_dir / "config.json").is_file():
|
||||
return str(transformer_dir)
|
||||
raise FileNotFoundError(f"Could not resolve a transformer directory from: {path}")
|
||||
|
||||
|
||||
def _resolve_backbone_ckpt(path: str) -> str:
|
||||
candidate = Path(path).expanduser().resolve()
|
||||
if candidate.is_file():
|
||||
return str(candidate)
|
||||
backbone_path = candidate / "backbone.pt"
|
||||
if backbone_path.is_file():
|
||||
return str(backbone_path)
|
||||
raise FileNotFoundError(f"Could not resolve backbone.pt from: {path}")
|
||||
|
||||
|
||||
def _find_index_file(model_dir: str) -> str | None:
|
||||
for filename in INDEX_FILENAMES:
|
||||
candidate = os.path.join(model_dir, filename)
|
||||
if os.path.isfile(candidate):
|
||||
return filename
|
||||
|
||||
matches = sorted(
|
||||
filename
|
||||
for filename in os.listdir(model_dir)
|
||||
if filename.endswith(".safetensors.index.json")
|
||||
)
|
||||
return matches[0] if matches else None
|
||||
|
||||
|
||||
def _load_weight_map(model_dir: str) -> tuple[dict[str, str], str | None]:
|
||||
index_filename = _find_index_file(model_dir)
|
||||
if index_filename is not None:
|
||||
with open(os.path.join(model_dir, index_filename), encoding="utf-8") as f:
|
||||
index_data = json.load(f)
|
||||
return dict(index_data["weight_map"]), index_filename
|
||||
|
||||
safetensors_files = sorted(
|
||||
filename
|
||||
for filename in os.listdir(model_dir)
|
||||
if filename.endswith(".safetensors")
|
||||
)
|
||||
if len(safetensors_files) != 1:
|
||||
raise ValueError(
|
||||
f"Expected an index file or a single safetensors shard in {model_dir}, "
|
||||
f"found {len(safetensors_files)} shard(s)."
|
||||
)
|
||||
|
||||
shard_name = safetensors_files[0]
|
||||
with safe_open(
|
||||
os.path.join(model_dir, shard_name), framework="pt", device="cpu"
|
||||
) as f:
|
||||
weight_map = {key: shard_name for key in f.keys()}
|
||||
index_filename = f"{Path(shard_name).stem}.safetensors.index.json"
|
||||
return weight_map, index_filename
|
||||
|
||||
|
||||
def _load_config(model_dir: str) -> dict:
|
||||
config_path = os.path.join(model_dir, "config.json")
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def get_default_keep_bf16_patterns(
|
||||
*, model_type: str, class_name: str | None
|
||||
) -> list[str]:
|
||||
if model_type == "flux1":
|
||||
return list(DEFAULT_FLUX1_KEEP_BF16_PATTERNS)
|
||||
if model_type == "flux2":
|
||||
return list(DEFAULT_FLUX2_KEEP_BF16_PATTERNS)
|
||||
if model_type == "none":
|
||||
return []
|
||||
if class_name == "FluxTransformer2DModel":
|
||||
return list(DEFAULT_FLUX1_KEEP_BF16_PATTERNS)
|
||||
if class_name == "Flux2Transformer2DModel":
|
||||
return list(DEFAULT_FLUX2_KEEP_BF16_PATTERNS)
|
||||
return []
|
||||
|
||||
|
||||
def should_keep_bf16(
|
||||
weight_name: str,
|
||||
keep_bf16_patterns: Sequence[str],
|
||||
) -> bool:
|
||||
if not keep_bf16_patterns:
|
||||
return False
|
||||
|
||||
module_name = weight_name[:-7] if weight_name.endswith(".weight") else weight_name
|
||||
return any(re.search(pattern, module_name) for pattern in keep_bf16_patterns)
|
||||
|
||||
|
||||
def is_ignored_by_modelopt(
|
||||
weight_name: str,
|
||||
ignore_patterns: Sequence[str],
|
||||
) -> bool:
|
||||
if not ignore_patterns:
|
||||
return False
|
||||
|
||||
module_name = weight_name[:-7] if weight_name.endswith(".weight") else weight_name
|
||||
for pattern in ignore_patterns:
|
||||
regex_str = pattern.replace(".", r"\.").replace("*", r".*")
|
||||
if re.fullmatch(regex_str, module_name):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def build_fp8_scale_map(
|
||||
model_state_dict: Mapping[str, torch.Tensor],
|
||||
*,
|
||||
maxbound: float = FP8_E4M3_MAXBOUND,
|
||||
) -> dict[str, dict[str, torch.Tensor]]:
|
||||
scale_map: dict[str, dict[str, torch.Tensor]] = {}
|
||||
for key, value in model_state_dict.items():
|
||||
if key.endswith(".weight_quantizer._amax"):
|
||||
layer_name = key[: -len(".weight_quantizer._amax")]
|
||||
scale_map.setdefault(f"{layer_name}.weight", {})["weight_scale"] = (
|
||||
value.detach().to(torch.float32).reshape(1).cpu() / maxbound
|
||||
)
|
||||
elif key.endswith(".input_quantizer._amax"):
|
||||
layer_name = key[: -len(".input_quantizer._amax")]
|
||||
scale_map.setdefault(f"{layer_name}.weight", {})["input_scale"] = (
|
||||
value.detach().to(torch.float32).reshape(1).cpu() / maxbound
|
||||
)
|
||||
|
||||
return {
|
||||
weight_name: scale_tensors
|
||||
for weight_name, scale_tensors in scale_map.items()
|
||||
if {"weight_scale", "input_scale"} <= set(scale_tensors)
|
||||
}
|
||||
|
||||
|
||||
def quantize_fp8_weight(
|
||||
weight: torch.Tensor,
|
||||
weight_scale: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
if weight.dtype == torch.float8_e4m3fn:
|
||||
return weight.contiguous()
|
||||
|
||||
scale = weight_scale.to(weight.device, dtype=torch.float32)
|
||||
if scale.numel() != 1:
|
||||
raise ValueError(
|
||||
"Only per-tensor FP8 scales are supported for diffusion checkpoints, "
|
||||
f"got shape {tuple(scale.shape)}."
|
||||
)
|
||||
|
||||
quantized = (weight.to(torch.float32) / scale.reshape(1)).to(torch.float8_e4m3fn)
|
||||
return quantized.cpu().contiguous()
|
||||
|
||||
|
||||
def _copy_non_shard_files(source_dir: str, output_dir: str) -> None:
|
||||
ignored = set(INDEX_FILENAMES)
|
||||
for entry in os.listdir(source_dir):
|
||||
if entry.endswith(".safetensors") or entry in ignored:
|
||||
continue
|
||||
source_path = os.path.join(source_dir, entry)
|
||||
output_path = os.path.join(output_dir, entry)
|
||||
if os.path.isdir(source_path):
|
||||
shutil.copytree(source_path, output_path, dirs_exist_ok=True)
|
||||
else:
|
||||
shutil.copy2(source_path, output_path)
|
||||
|
||||
|
||||
def _load_selected_tensors(
|
||||
model_dir: str,
|
||||
weight_map: Mapping[str, str],
|
||||
tensor_names: Iterable[str],
|
||||
) -> dict[str, torch.Tensor]:
|
||||
tensors: dict[str, torch.Tensor] = {}
|
||||
names_by_file: dict[str, list[str]] = defaultdict(list)
|
||||
for name in tensor_names:
|
||||
names_by_file[weight_map[name]].append(name)
|
||||
|
||||
for filename, names in names_by_file.items():
|
||||
shard_path = os.path.join(model_dir, filename)
|
||||
with safe_open(shard_path, framework="pt", device="cpu") as f:
|
||||
for name in names:
|
||||
tensors[name] = f.get_tensor(name).contiguous()
|
||||
return tensors
|
||||
|
||||
|
||||
def convert_modelopt_fp8_checkpoint(
|
||||
*,
|
||||
modelopt_hf_dir: str,
|
||||
modelopt_backbone_ckpt: str,
|
||||
output_dir: str,
|
||||
base_transformer_dir: str | None = None,
|
||||
model_type: str = "auto",
|
||||
keep_bf16_patterns: Sequence[str] | None = None,
|
||||
maxbound: float = FP8_E4M3_MAXBOUND,
|
||||
overwrite: bool = False,
|
||||
) -> dict[str, int]:
|
||||
source_dir = _resolve_transformer_dir(modelopt_hf_dir)
|
||||
backbone_ckpt_path = _resolve_backbone_ckpt(modelopt_backbone_ckpt)
|
||||
base_dir = (
|
||||
_resolve_transformer_dir(base_transformer_dir) if base_transformer_dir else None
|
||||
)
|
||||
|
||||
config = _load_config(source_dir)
|
||||
quant_config = config.get("quantization_config")
|
||||
if not isinstance(quant_config, dict):
|
||||
raise ValueError(
|
||||
"Expected a flat quantization_config dict in the ModelOpt export."
|
||||
)
|
||||
if (
|
||||
quant_config.get("quant_method") != "modelopt"
|
||||
or "FP8" not in str(quant_config.get("quant_algo", "")).upper()
|
||||
):
|
||||
raise ValueError(
|
||||
"This tool only supports ModelOpt diffusers FP8 exports "
|
||||
"(quant_method=modelopt, quant_algo=FP8)."
|
||||
)
|
||||
|
||||
class_name = config.get("_class_name")
|
||||
ignore_patterns = list(quant_config.get("ignore", []) or [])
|
||||
patterns = list(
|
||||
get_default_keep_bf16_patterns(model_type=model_type, class_name=class_name)
|
||||
)
|
||||
if keep_bf16_patterns:
|
||||
patterns.extend(keep_bf16_patterns)
|
||||
if patterns and base_dir is None:
|
||||
raise ValueError(
|
||||
"BF16 fallback patterns are enabled, but --base-transformer-dir was not provided."
|
||||
)
|
||||
|
||||
output_path = Path(output_dir).expanduser().resolve()
|
||||
if output_path.exists():
|
||||
if not overwrite:
|
||||
raise FileExistsError(
|
||||
f"Output directory already exists: {output_path}. "
|
||||
"Use --overwrite to replace it."
|
||||
)
|
||||
shutil.rmtree(output_path)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
_copy_non_shard_files(source_dir, str(output_path))
|
||||
|
||||
source_weight_map, index_filename = _load_weight_map(source_dir)
|
||||
base_weight_map: dict[str, str] = {}
|
||||
if base_dir is not None:
|
||||
base_weight_map, _ = _load_weight_map(base_dir)
|
||||
|
||||
backbone_state = torch.load(backbone_ckpt_path, map_location="cpu")[
|
||||
"model_state_dict"
|
||||
]
|
||||
fp8_scale_map = build_fp8_scale_map(backbone_state, maxbound=maxbound)
|
||||
serialized_quant_config = json.dumps(quant_config, sort_keys=True)
|
||||
|
||||
fallback_weight_names = sorted(
|
||||
weight_name
|
||||
for weight_name in source_weight_map
|
||||
if weight_name.endswith(".weight") and should_keep_bf16(weight_name, patterns)
|
||||
)
|
||||
fallback_tensors = (
|
||||
_load_selected_tensors(base_dir, base_weight_map, fallback_weight_names)
|
||||
if fallback_weight_names
|
||||
else {}
|
||||
)
|
||||
fallback_scale_names = {
|
||||
scale_name
|
||||
for weight_name in fallback_weight_names
|
||||
for scale_name in (
|
||||
weight_name[:-7] + ".weight_scale",
|
||||
weight_name[:-7] + ".input_scale",
|
||||
)
|
||||
}
|
||||
|
||||
weights_by_file: dict[str, list[str]] = defaultdict(list)
|
||||
for weight_name, filename in source_weight_map.items():
|
||||
weights_by_file[filename].append(weight_name)
|
||||
|
||||
updated_weight_map: dict[str, str] = {}
|
||||
total_size = 0
|
||||
added_scale_count = 0
|
||||
preserved_ignored_weight_count = 0
|
||||
|
||||
for filename, names in sorted(weights_by_file.items()):
|
||||
shard_path = os.path.join(source_dir, filename)
|
||||
shard_tensors = load_file(shard_path, device="cpu")
|
||||
|
||||
with safe_open(shard_path, framework="pt", device="cpu") as f:
|
||||
metadata = dict(f.metadata() or {})
|
||||
|
||||
metadata.setdefault("format", "pt")
|
||||
metadata["quantization_config"] = serialized_quant_config
|
||||
metadata["_quantization_metadata"] = serialized_quant_config
|
||||
|
||||
for name in list(shard_tensors.keys()):
|
||||
if "_quantizer." in name:
|
||||
del shard_tensors[name]
|
||||
continue
|
||||
if name in fallback_scale_names:
|
||||
del shard_tensors[name]
|
||||
continue
|
||||
if name.endswith(".weight") and is_ignored_by_modelopt(
|
||||
name, ignore_patterns
|
||||
):
|
||||
preserved_ignored_weight_count += 1
|
||||
continue
|
||||
if name in fallback_tensors:
|
||||
shard_tensors[name] = fallback_tensors[name]
|
||||
if (
|
||||
name.endswith(".weight")
|
||||
and name in fp8_scale_map
|
||||
and name not in fallback_tensors
|
||||
):
|
||||
scale_tensors = fp8_scale_map[name]
|
||||
shard_tensors[name] = quantize_fp8_weight(
|
||||
shard_tensors[name], scale_tensors["weight_scale"]
|
||||
)
|
||||
shard_tensors[name[:-7] + ".weight_scale"] = scale_tensors[
|
||||
"weight_scale"
|
||||
]
|
||||
shard_tensors[name[:-7] + ".input_scale"] = scale_tensors["input_scale"]
|
||||
added_scale_count += 2
|
||||
|
||||
save_file(shard_tensors, os.path.join(output_path, filename), metadata=metadata)
|
||||
|
||||
for name, tensor in shard_tensors.items():
|
||||
updated_weight_map[name] = filename
|
||||
total_size += tensor.element_size() * tensor.numel()
|
||||
|
||||
del shard_tensors
|
||||
gc.collect()
|
||||
|
||||
with open(output_path / index_filename, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
{
|
||||
"metadata": {"total_size": total_size},
|
||||
"weight_map": updated_weight_map,
|
||||
},
|
||||
f,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
return {
|
||||
"quantized_weights": sum(
|
||||
1
|
||||
for name in source_weight_map
|
||||
if name.endswith(".weight")
|
||||
and name in fp8_scale_map
|
||||
and not is_ignored_by_modelopt(name, ignore_patterns)
|
||||
),
|
||||
"bf16_fallback_weights": len(fallback_weight_names),
|
||||
"preserved_ignored_weights": preserved_ignored_weight_count,
|
||||
"added_scale_tensors": added_scale_count,
|
||||
"output_shards": len(weights_by_file),
|
||||
}
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Inject FP8 scales from ModelOpt backbone.pt into a diffusers export so "
|
||||
"SGLang diffusion can load it natively."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--modelopt-hf-dir",
|
||||
required=True,
|
||||
help="ModelOpt --hf-ckpt-dir output, or its transformer subdirectory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--modelopt-backbone-ckpt",
|
||||
required=True,
|
||||
help="Path to backbone.pt, or the directory that contains it.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
required=True,
|
||||
help="Directory to write the converted SGLang transformer checkpoint.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--base-transformer-dir",
|
||||
help=(
|
||||
"Original BF16 transformer directory (or parent model dir). Required when "
|
||||
"BF16 fallback layers are enabled."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model-type",
|
||||
choices=["auto", "flux1", "flux2", "none"],
|
||||
default="auto",
|
||||
help=(
|
||||
"Optional model-family BF16 fallback profile. 'none' uses the generic "
|
||||
"conversion path. 'auto' enables the validated FLUX.1 / FLUX.2 "
|
||||
"fallback set when the export config matches those transformer classes."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--keep-bf16-pattern",
|
||||
action="append",
|
||||
default=[],
|
||||
help=(
|
||||
"Regex matched against module names without the trailing .weight. "
|
||||
"Matching weights are copied from --base-transformer-dir instead of "
|
||||
"staying in FP8."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--maxbound",
|
||||
type=float,
|
||||
default=FP8_E4M3_MAXBOUND,
|
||||
help="FP8 maxbound used to turn ModelOpt amax into a scale. E4M3 uses 448.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="Replace --output-dir if it already exists.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = _parse_args()
|
||||
stats = convert_modelopt_fp8_checkpoint(
|
||||
modelopt_hf_dir=args.modelopt_hf_dir,
|
||||
modelopt_backbone_ckpt=args.modelopt_backbone_ckpt,
|
||||
output_dir=args.output_dir,
|
||||
base_transformer_dir=args.base_transformer_dir,
|
||||
model_type=args.model_type,
|
||||
keep_bf16_patterns=args.keep_bf16_pattern,
|
||||
maxbound=args.maxbound,
|
||||
overwrite=args.overwrite,
|
||||
)
|
||||
print(json.dumps(stats, indent=2, sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user