The model was converted to nvfp4 using llmcompressor and script below on a dual H200 runpod instance. It can generate outputs unsuitable for all audiences.
import os import sys import shutil
from datasets import concatenate_datasets, load_dataset from huggingface_hub import snapshot_download from transformers import AutoModelForImageTextToText, AutoTokenizer
from llmcompressor import oneshot from llmcompressor.modifiers.quantization import QuantizationModifier from llmcompressor.utils import dispatch_for_generation
Available Qwen3.5 MoE models (pick one):
"Qwen/Qwen3.5-35B-A3B"
"Qwen/Qwen3.5-122B-A10B"
"Qwen/Qwen3.5-397B-A17B"
#MODEL_ID = "Qwen/Qwen3.5-35B-A3B" MODEL_ID = sys.argv[1]
Load model.
model = AutoModelForImageTextToText.from_pretrained(MODEL_ID, dtype="auto") tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
Select number of samples. 512 is recommended for production quality;
reduce to 256 or lower for faster iteration during development.
NUM_CALIBRATION_SAMPLES = 256 MAX_SEQUENCE_LENGTH = 4096
Load datasets and preprocess.
Use half from each source for a diverse calibration set.
samples_per_dataset = NUM_CALIBRATION_SAMPLES // 2
ds_ultrachat = load_dataset( "HuggingFaceH4/ultrachat_200k", split=f"train_sft[:{samples_per_dataset}]", ) ds_nemotron = load_dataset( "nvidia/Nemotron-Post-Training-Dataset-v2", split=f"chat[:{samples_per_dataset}]", )
Both datasets share a "messages" column with the same chat format.
Keep only that column so we can concatenate them.
ds_ultrachat = ds_ultrachat.select_columns(["messages"]) ds_nemotron = ds_nemotron.select_columns(["messages"]) ds = concatenate_datasets([ds_ultrachat, ds_nemotron]) ds = ds.shuffle(seed=42)
def preprocess(example): return { "text": tokenizer.apply_chat_template( example["messages"], tokenize=False, ) }
ds = ds.map(preprocess)
Tokenize inputs.
def tokenize(sample): return tokenizer( sample["text"], padding=False, max_length=MAX_SEQUENCE_LENGTH, truncation=True, add_special_tokens=False, )
ds = ds.map(tokenize, remove_columns=ds.column_names)
Configure the quantization algorithm and scheme.
In this case, we:
* quantize the weights to fp4 with per group 16 via ptq
* calibrate a global_scale for activations, which will be used to
quantize activations to fp4 on the fly
recipe = QuantizationModifier( targets="Linear", scheme="NVFP4", ignore=[ "lm_head", "re:.*mlp.gate$", "re:.mlp.shared_expert_gate$", "re:.linear_attn.", "re:model\.visual\..", ], )
Apply quantization.
MoE calibration is now handled automatically by the pipeline.
We set moe_calibrate_all_experts to True to ensure all experts receive
calibration data. This temporarily updates the model definition to use
CalibrationQwen3_5MoeSparseMoeBlock (from llmcompressor.modeling.qwen3_5_moe)
which replaces the original Qwen3_5MoeSparseMoeBlock class.
This unfuses the 3D expert parameters into individual nn.Linear modules
so they can be targeted by quantization.
Feel free to update the definition under
llm-compressor/src/llmcompressor/modeling/qwen3_5_moe.py to play around with
this behavior and evaluate its impact on quantization performance.
oneshot( model=model, dataset=ds, recipe=recipe, max_seq_length=MAX_SEQUENCE_LENGTH, num_calibration_samples=NUM_CALIBRATION_SAMPLES, moe_calibrate_all_experts=True, )
print("\n\n") print("========== SAMPLE GENERATION ==============") try: dispatch_for_generation(model) input_ids = tokenizer("Hello my name is", return_tensors="pt").input_ids.to( model.device ) output = model.generate(input_ids, max_new_tokens=100) print(tokenizer.decode(output[0])) except Exception as e: print(f"Generation failed (non-fatal): {e}") print("==========================================\n\n")
Save to disk in compressed-tensors format.
MTP (multi-token prediction) weights are automatically preserved from
the source checkpoint during save, enabling speculative decoding with vLLM.
SAVE_DIR = MODEL_ID.rstrip("/").split("/")[-1] + "-NVFP4" model.save_pretrained(SAVE_DIR, save_compressed=True) tokenizer.save_pretrained(SAVE_DIR)
Hot-fix: copy processor configs that save_pretrained doesn't bring over
cache_dir = snapshot_download(MODEL_ID, allow_patterns=["*.json"]) for filename in [ "preprocessor_config.json", "video_preprocessor_config.json", ]: src = os.path.join(cache_dir, filename) if os.path.exists(src): shutil.copyfile(src, os.path.join(SAVE_DIR, filename))