Files
Model-Optimizer/examples/vllm_serve/README.md
T
kinjalpatel27 ad9ea97a4b Fix vLLM compilation guard for models without marker (#2518)
### What does this PR do?

Type of change: Bug fix

Makes the vLLM `disable_compilation` context manager support inner model
implementations that do not predefine a `do_not_compile` attribute,
including GLM-5.3. The context manager now installs the marker
temporarily and removes it afterward, while preserving and restoring
existing marker values for other vLLM models.

Adds regression coverage for both supported wrapper layouts:
`model.model` and `model.language_model.model`.

### Usage

```python
with disable_compilation(model):
    mtq.quantize(model, quant_cfg, forward_loop=calibrate_loop)
```

No caller changes are required.

### Testing

- Ran `tests/gpu_vllm/torch/quantization/test_vllm_dynamic_modules.py`:
24 passed with vLLM 0.28.
- Ran pre-commit on both changed files: all applicable hooks passed.
- Installed this branch into `vllm/vllm-openai:glm53-flash` on OCI-JHB
and served the GLM-5.3-Flash BF16 checkpoint with
`QUANT_CFG=NVFP4_DEFAULT_CFG`, TP=4, eager mode, and BF16 KV cache.
- GLM passed the previous `do_not_compile` failure point, inserted 1,700
quantizers, enabled 456 weight quantizers, reached a healthy API server,
and returned a relevant manual prompt response.

### Before your PR is "*Ready for review*"

Make sure you read and follow [Contributor
guidelines](https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md)
and your commits are signed (`git commit -s -S`).

Make sure you read and follow the [Security Best
Practices](https://github.com/NVIDIA/Model-Optimizer/blob/main/SECURITY.md#security-coding-practices-for-contributors)
(e.g. avoiding hardcoded `trust_remote_code=True`, `torch.load(...,
weights_only=False)`, `pickle`, etc.).

- Is this change backward compatible?: ✅
- If you copied code from any other sources or added a new PIP
dependency, did you follow guidance in `CONTRIBUTING.md`: N/A
- Did you write any new necessary tests?: ✅
- Did you update
[Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?:
N/A — integration compatibility fix; no user-facing API change.
- Did you get Claude approval on this PR?: N/A

### Additional Information

Validated against GLM-5.3-Flash using ModelOpt commit
`869b64fcee0b20be323663449b00e8c52940a289`.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Compilation settings are now handled across supported nested model
configurations and restored after calibration, including when errors
occur.
- Calibration inputs correctly exclude padding when an attention mask is
provided and reject empty sequences.
- vLLM warmup reserves the required cache space for supported tail-cache
configurations.
- Serving startup supports an alternate vLLM launcher import path when
the OpenAI entrypoint is unavailable.
- **Compatibility**
- The vLLM serving example now defaults to vLLM 0.30.0 and documents
tested support for Nemotron 3 Nano hybrid attention/Mamba serving on
vLLM 0.26.0 and 0.30.0.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Kinjal Patel <kinjalpravin@nvidia.com>
2026-09-29 14:28:32 -07:00

287 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Serve fakequant models with vLLM
This is a simple example to demonstrate calibrating and serving ModelOpt fakequant models in vLLM.
Compared with realquant, fakequant is 2-5x slower, but doesn't require dedicated kernel support and facilitates research.
The general fakequant example is tested with vLLM 0.9.0, 0.19.1, 0.26.0, 0.28.0, 0.29.0 and 0.30.0. The
compact NVFP4 attention worker documented below requires vLLM 0.15.0 or newer.
## Prepare environment
Use the Dockerfile to build an environment with vLLM 0.30.0:
```bash
docker build -f examples/vllm_serve/Dockerfile -t vllm-modelopt:v0.30.0 .
```
To build the same environment with another tested vLLM release, override `VLLM_VERSION`:
```bash
docker build --build-arg VLLM_VERSION=0.26.0 \
-f examples/vllm_serve/Dockerfile -t vllm-modelopt:v0.26.0 .
```
For a direct installation from the ModelOpt repository root, install the tested vLLM
release and the ModelOpt extras used by this example:
```bash
python3 -m pip install "vllm==0.30.0"
python3 -m pip install -e ".[all,mlflow]"
```
See the [ModelOpt installation guide](../../docs/source/getting_started/_installation_for_Linux.rst)
for details about installing partial dependency sets.
## Calibrate and serve fake quant model in vLLM
Step 1: Configure quantization settings.
You can either edit the `quant_config` dictionary in `vllm_serve_fakequant.py`, or set the following environment variables to control quantization behavior:
| Variable | Description | Default |
|-----------------|--------------------------------------------------|---------------------|
| QUANT_DATASET | Dataset name for calibration | cnn_dailymail |
| QUANT_CALIB_SIZE| Number of samples used for calibration | 512 |
| QUANT_CFG | Quantization config | None |
| KV_QUANT_CFG | KV-cache quantization config | None |
| QUANT_FILE_PATH | Optional path to exported quantizer state dict `quantizer_state.pth` | None |
| MODELOPT_STATE_PATH | Optional path to exported `vllm_fq_modelopt_state.pth` (restores quantizer state and parameters) | None |
| CALIB_BATCH_SIZE | Calibration batch size | 1 |
| RECIPE_PATH | Optional path to a ModelOpt PTQ recipe YAML | None |
Set these variables in your shell or Docker environment as needed to customize calibration.
Step 2: Run the following command, with all supported flag as `vllm serve`:
```bash
python vllm_serve_fakequant.py <model_path> -tp 8 --host 0.0.0.0 --port 8000
```
Hybrid attention/Mamba models such as Nemotron 3 Nano are supported on vLLM 0.26.0, 0.28.0, 0.29.0 and
0.30.0. For example, calibrate and serve with NVFP4 KV-cache fakequant as follows:
```bash
KV_QUANT_CFG=NVFP4_KV_CFG QUANT_CALIB_SIZE=512 \
python vllm_serve_fakequant.py <nemotron3_nano_model_path> -tp 8 \
--max-model-len 8192 --enforce-eager --host 0.0.0.0 --port 8000
```
Calibration uses dedicated scratch KV-cache blocks, so reducing `--max-num-batched-tokens`
is not required to avoid NaNs.
For vLLM versions that expose `--moe-backend`, this launcher defaults to `--moe-backend triton`.
ModelOpt expert fakequant needs a decomposed MoE backend so both expert GEMMs are visible during
calibration.
Step 3: test the API server with curl:
```bash
curl -X POST "http://127.0.0.1:8000/v1/chat/completions" -H "Content-Type: application/json" -d '{
"model": "<model_path>",
"messages": [
{"role": "user", "content": "Hi, what is your name"}
],
"max_tokens": 8
}'
```
Step 4 (Optional): using lm_eval to run evaluation
```bash
lm_eval --model local-completions --tasks gsm8k --model_args model=<model_name>,base_url=http://127.0.0.1:8000/v1/completions,num_concurrent=1,max_retries=3,tokenized_requests=False,batch_size=128,tokenizer_backend=None
```
## Tracking a serve with MLflow
Pass `--mlflow <tracking-uri>`, or set MLflow's own `MLFLOW_TRACKING_URI`, to record what
this server actually quantized, so the numbers an evaluation produces can be traced back to
a recipe:
```bash
RECIPE_PATH=<PATH_TO_RECIPE> python vllm_serve_fakequant.py <model_path> -tp 8 \
--host 0.0.0.0 --port 8000 \
--mlflow https://<your-mlflow-server>/
```
This is the *quantization* tracking server. It is unrelated to any tracking server an
evaluation harness exports its scores to — NeMo Evaluator Launcher, for instance, has its
own `export.mlflow.tracking_uri`. Keep the two separate.
Quantization runs in the vLLM **worker**, not in `vllm_serve_fakequant.py`, so that is where
the run is recorded: the launcher validates the URI and hands the settings to the workers
through the environment, and global rank 0 opens the run. It opens *before the weights
load*, so a bad URI or a missing token fails within seconds rather than after a load and a
full calibration, and it closes `FINISHED` once the model is quantized and warmed up —
serving itself is not tracked.
<details>
<summary>Uploaded artifacts</summary>
| Artifact | Contents |
| --- | --- |
| `command.txt` | The launcher's full invocation, copy-pasteable, with credentials masked |
| `version.txt` | The ModelOpt version that ran |
| `recipe/resolved_recipe.yaml` | `RECIPE_PATH` with its `$import`s expanded, so it stands alone |
| `recipe/quant_cfg.yaml` | `QUANT_CFG` and `KV_QUANT_CFG` merged, plus any MLA fixup — only when no recipe is used, since a recipe's config is already in `resolved_recipe.yaml` |
| `logs/<script>.log` | The rank-0 worker's Python stdout/stderr, including the traceback if it crashed |
| `summary/quant_summary.txt` | The per-quantizer summary |
</details>
The quantization settings from the table above are logged as searchable params, alongside
the serving settings (`tensor_parallel_size`, `max_model_len`, `dtype`, `kv_cache_dtype`, …)
and `user` / `hostname` / `modelopt_version` / `git_sha` / `vllm_version` tags. The
`checkpoint_path` tag is the checkpoint being served, which is the same key
`examples/hf_ptq/hf_ptq.py` tags its runs with — so the PTQ run that produced a checkpoint
and every serve of it can be found together.
Other flags:
- `--mlflow_experiment` — defaults to
`$USER/vllm_serve_fakequant/<model basename>-<recipe name>`, falling back to
`$QUANT_CFG`/`$KV_QUANT_CFG` when no recipe is used.
- `--mlflow_run_name` — defaults to the UTC start time, `YYYYmmdd-HHMMSS`.
- `$MLFLOW_TRACKING_URI` enables tracking on its own; `--mlflow` overrides it. A URI taken
from the environment is best-effort — if the client is missing or the server is
unreachable the server warns and serves untracked. An explicit `--mlflow` fails loudly
instead.
Tracking needs the client: `pip install nvidia-modelopt[mlflow]` (already in this example's
`Dockerfile`). Authentication uses MLflow's own environment variables
(`MLFLOW_TRACKING_TOKEN`, or `MLFLOW_TRACKING_USERNAME` / `MLFLOW_TRACKING_PASSWORD`); with
`--distributed-executor-backend ray` those are forwarded to the workers along with the
tracking settings, since a Ray worker starts with a clean environment.
## Load QAT/PTQ model and serve in vLLM (WIP)
Step 1: export the model with bf16 weights and quantizer state. To export the model:
- For **HF** models, use `examples/hf_ptq/hf_ptq.py` with `--vllm_fakequant_export`:
```bash
python ../hf_ptq/hf_ptq.py \
--pyt_ckpt_path <MODEL_PATH> \
--recipe <PATH_TO_RECIPE> \
--calib_size 512 \
--export_path <EXPORT_DIR> \
--vllm_fakequant_export \
--trust_remote_code
```
This creates `<EXPORT_DIR>/vllm_fq_modelopt_state.pth` (ModelOpt quantizer state for vLLM fake-quant reload) and saves the HF-exported model under `<EXPORT_DIR>` (config/tokenizer/weights).
Note: `--pyt_ckpt_path` can point to either an HF checkpoint or a ModelOpt-saved checkpoint (e.g., a QAT/QAD checkpoint produced by `examples/llm_qat/train.py`). If the input checkpoint is already quantized, the script will **skip re-quantization** and only export artifacts for vLLM fakequant reload.
- For **MCore** models, export the model with flag `--export-vllm-fq` as described in [Megatron-LM README](https://github.com/NVIDIA/Megatron-LM/tree/main/examples/post_training/modelopt#-nvfp4-quantization-qauntization-aware-training-and-model-export). This generates `quantizer_state.pth`, which contains quantizer tensors for vLLM reload via `QUANT_FILE_PATH`.
Step 2: use the exported artifacts when serving:
- **HF export**: pass the exported `vllm_fq_modelopt_state.pth` via `MODELOPT_STATE_PATH`
```bash
# HF
MODELOPT_STATE_PATH=<vllm_fq_modelopt_state.pth> python vllm_serve_fakequant.py <model_path> -tp 8 --host 0.0.0.0 --port 8000
```
- **MCore export**: pass the exported `quantizer_state.pth` via `QUANT_FILE_PATH` and set `QUANT_CFG` to match the MCore quantization recipe
```bash
# MCore
QUANT_CFG=<quant_cfg> QUANT_FILE_PATH=<quantizer_state.pth> python vllm_serve_fakequant.py <model_path> -tp 8 --host 0.0.0.0 --port 8000
```
## Serve a model with sparse attention in vLLM
Apply ModelOpt sparse attention at serve time. Right after model load, the launcher replaces each native attention implementation with its matching ModelOpt adapter: `ModelOptSparseAttentionImpl` for FlashAttention or `ModelOptSparseFlashInferImpl` for FlashInfer. Both adapters use the same Triton kernel with paged KV cache support.
The configuration is read from the checkpoint's `config.json` `sparse_attention_config` block, written by ModelOpt's HF export. The launcher restores calibrated skip-softmax metadata and N:M sparse-softmax metadata (`sparsity_n`, `sparsity_m`, `dense_sink_tokens`, `dense_recent_tokens`). Checkpoints exported with both metadata entries use ModelOpt Triton for sparse prefill launches; launches without active sparse work delegate back to the native backend selected by vLLM.
Workflow:
1. Calibrate and export the model with `examples/llm_sparsity/attention_sparsity/hf_sa.py`. This writes `sparse_attention_config` into the exported checkpoint's `config.json`.
2. Serve the exported checkpoint with `--enforce-eager` (CUDA graph capture is not yet validated with the sparse attention kernel — see Known Problems):
```bash
python vllm_serve_sparse_attn.py <EXPORT_DIR> --enforce-eager -tp 8 --host 0.0.0.0 --port 8000
```
If the checkpoint has no `sparse_attention_config`, the sparse-only installer passes through and vLLM runs unchanged. Whole-model fakequant flows remain handled by `vllm_serve_fakequant.py`; the compact attention-only path is below.
### Calibrate skip-softmax thresholds through vLLM
Instead of the HF path in step 1, thresholds can be calibrated directly through vLLM — over the paged KV cache, for both prefill and decode, with tensor parallelism. Pipeline and data parallelism are not supported by calibration.
```bash
# One-time: fetch the RULER essay haystack
bash ../llm_sparsity/attention_sparsity/download_ruler_data.sh
python calibrate_sparse_attn.py <CKPT> \
--calib_data_dir ../llm_sparsity/attention_sparsity/data \
--target_sparse_ratio 0.5 \
--decode_tokens 32 --tensor_parallel_size 8 --update_checkpoint_config
```
Calibration always writes `sparse_attention_config.json` in the current directory.
`--update_checkpoint_config` also merges that configuration into `<CKPT>/config.json` in
place, which lets `vllm_serve_sparse_attn.py` load it automatically. This option requires
`<CKPT>` to be a local checkpoint directory; without it, merge the generated configuration
into the checkpoint manually before serving.
Calibration prompts default to the **RULER dataset** via the same `RulerDatasetBuilder` the HF calibration path uses (`--calib_samples` / `--calib_max_seqlen` mirror the HF defaults of 24 / 32768), so vLLM- and PyTorch-calibrated thresholds are fit on identical data. `--prompts_file` (one prompt per line) substitutes custom calibration data.
`install_vllm_skip_softmax_calibration` (called by `sparse_attn_worker.SkipSoftmaxCalibWorker` at model load) swaps calibration adapters onto each attention layer not listed in the checkpoint's existing skip-softmax `ignore` policy after validating all selected layers — eager execution is required, model and KV-cache dtypes must be fp16/bf16, and no attention Q/K/P/V fakequant may be active. During `llm.generate`, the paged Triton calibration kernel computes full dense attention — no sparsification is applied to generation, though the dense kernel's numerics differ slightly from the native backend's — while counting, per candidate threshold, how many KV tiles the skip criterion would drop. The driver then collects **raw tile counts from every TP rank** (each rank only measures its head shard), merges them, fits `scale_factor = a * exp(b * sparsity)` once per phase, and writes the same canonical `sparse_attention_config` block the HF export produces — preserving the existing skip-softmax layer policy and any exported N:M sparse-softmax groups — so the serving workflow above picks it up unchanged.
Calibration and serving use the same 128-token KV-tile skip granularity and the same 128-row Q tile for prefill, so serving realizes the calibrated skip decision. One-token decode uses a 16-row Q compute tile because its padding rows cannot affect the decision. Serving autotunes only the execution schedule (`num_warps` / `num_stages`); measurement remains a single fixed launch because its counters have side effects.
The reusable serving policies live in `modelopt/torch/sparsity/attention_sparsity/plugins/vllm_runtime.py`. `install_vllm_sparse_attention_from_checkpoint` installs checkpoint-driven sparse-only attention, while `install_vllm_nvfp4_attention` installs fixed NVFP4 Q/K/P/V with optional checkpoint sparsity. Both validate every selected layer before publishing any replacement implementation and return a `VllmAttentionInstallReport` with the installed layer names and backend counts.
`sparse_attn_worker.py` only invokes these APIs after vLLM loads the model. It retains `SparseAttnWorker` as the launcher's default and provides `QuantSparseAttnWorker` for the compact NVFP4 policy. Other vLLM integrations can invoke the same library APIs directly:
```python
from modelopt.torch.sparsity.attention_sparsity.plugins.vllm_runtime import (
install_vllm_nvfp4_attention,
)
report = install_vllm_nvfp4_attention(model_runner, sparse_cfg="checkpoint")
```
Limitations:
- vLLM V1 chunked prefill and prefix-cache suffix attention are supported by offsetting query positions into the longer KV span. This applies to sparse-only serving; quantized attention installs and skip-softmax calibration reject `enable_prefix_caching` (quantize-on-write and per-request measurement both require uncached prefills).
- Skip-softmax calibration requires pipeline- and data-parallel size 1 because raw count records align only across tensor-parallel head shards; data-parallel replicas serve different requests.
- `SparseAttnWorker` CUDA graph capture is not validated yet — use `--enforce-eager`. Checkpoints with a calibrated `decode` `threshold_scale_factor` are rejected at install under a FULL decode CUDA graph mode (including vLLM's default `FULL_AND_PIECEWISE`): the captured graph would replay one request's stale threshold.
- Sparse-only installs validate engine-level compatibility like quantized installs do: decode context parallelism, DBO, speculative decoding, and FULL mixed-batch CUDA graphs are rejected (prefix caching remains supported, per the bullet above).
### Compact NVFP4 attention worker
vLLM 0.15.0 or newer is required when either worker activates a ModelOpt attention transform. Importing `SparseAttnWorker`, or using it with no checkpoint sparse metadata, does not resolve quant-only APIs.
Use the same launcher with the compact worker. By default, vLLM selects the backend for the model and platform; NemotronH on Blackwell selects FlashInfer:
```bash
python vllm_serve_sparse_attn.py <MODEL_PATH> -tp 8 \
--no-enable-prefix-caching \
--worker-cls sparse_attn_worker.QuantSparseAttnWorker
```
The installer supports both FlashInfer and FlashAttention, and the worker prints the installed adapter counts. Pass `--attention-backend FLASHINFER` or `--attention-backend FLASH_ATTN` only when an explicit override is needed.
This attention-only path applies a fixed dynamic block-16 NVFP4 fakequant format to Q/K/P/V. Q is dynamic; missing K/V scales default to global scale 1.0, and P defaults to amax 1.0. Existing scalar attention amax values are preserved, but this path does not calibrate or restore them itself. It does not re-quantize realquant Linear or MoE weights. An optional checkpoint `sparse_attention_config` is still honored for N:M sparse softmax; calibrated skip-softmax groups are rejected in combination with attention quantization, because quantized Q/K/P change the score distribution the skip thresholds were calibrated on.
Decode uses a fixed 32-split, 128-key-tile schedule. P QDQ consumes split-local,
unnormalized online-softmax probabilities, so changing that schedule can change
quantized results; split count is part of the numerical contract.
K is QDQ before its cache write, while V is written pristine. Complete 16-token V groups are finalized once in cache; an incomplete tail remains pristine and is QDQ on read. P@V therefore sees uniform fakequant values without re-quantizing the tail.
Supported configurations are regular decoder self-attention with FlashInfer or FlashAttention, fp16/bf16 model and KV cache, equal Q/K/V head dimensions that are multiples of 16, and DCP 1. The FlashInfer adapter preserves both NHD and HND cache strides and separates mixed decode/prefill launches so each phase keeps its own kernel contract. The default `FULL_AND_PIECEWISE` mode remains enabled for fixed N:M and attention-only NVFP4; checkpoints with calibrated decode `threshold_scale_factor` must use a non-`FULL` decode graph mode such as `--enforce-eager` because the live sequence length is not replayed as a Python scalar.
Unsupported features are sliding window, ALiBi, softcap, sinks, FP8 KV cache, cross/encoder/MLA attention, KV sharing or transfer, prefix caching, speculative decoding, DBO/ubatching, and `FULL` mixed/prefill CUDA graphs.
## Known Problems
1. **MCore reload does not use `MODELOPT_STATE_PATH`**; use `QUANT_FILE_PATH` and make sure `QUANT_CFG` matches the quantization recipe used for the original MCore model (otherwise quantizer keys/config won’t align).
2. KV cache quantization export and reload is not supported in MCore yet.
3. **`NVFP4_KV_CFG` and `NVFP4_AFFINE_KV_CFG` require `--enforce-eager`**; these configs use a dynamic-block Triton kernel for KV-cache quantization that is incompatible with CUDA graph capture (the kernel grid is computed from Python-level tensor shapes, which get baked in at capture time). Without `--enforce-eager`, the captured grid will be wrong for different batch sizes, producing incorrect outputs.