Instructions to use syaffers/gemma-3-4b-it-NVFP4 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use syaffers/gemma-3-4b-it-NVFP4 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="syaffers/gemma-3-4b-it-NVFP4") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("syaffers/gemma-3-4b-it-NVFP4") model = AutoModelForMultimodalLM.from_pretrained("syaffers/gemma-3-4b-it-NVFP4", device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use syaffers/gemma-3-4b-it-NVFP4 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "syaffers/gemma-3-4b-it-NVFP4" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "syaffers/gemma-3-4b-it-NVFP4", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/syaffers/gemma-3-4b-it-NVFP4
- SGLang
How to use syaffers/gemma-3-4b-it-NVFP4 with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "syaffers/gemma-3-4b-it-NVFP4" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "syaffers/gemma-3-4b-it-NVFP4", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "syaffers/gemma-3-4b-it-NVFP4" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "syaffers/gemma-3-4b-it-NVFP4", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use syaffers/gemma-3-4b-it-NVFP4 with Docker Model Runner:
docker model run hf.co/syaffers/gemma-3-4b-it-NVFP4
gemma-3-4b-it-NVFP4
Model Overview
- Model Architecture: Gemma3ForConditionalGeneration
- Input: Text, Image
- Output: Text
- Model Optimizations:
- Weight quantization: NVFP4
- Activation quantization: NVFP4
- Intended Use Cases: Intended for commercial and research use.
- Out-of-scope: Use in any manner that violates applicable laws or regulations (including trade compliance laws).
- Release Date: 2026-08-02
- Version: 1.0
- Model Developers: Syafiq Kamarul Azman, Claude
Quantized version of google/gemma-3-4b-it, targeting NVIDIA Blackwell GPUs.
Model Optimizations
This model was obtained by quantizing the weights and activations of the linear layers in the language model of google/gemma-3-4b-it to NVFP4 data type, using llm-compressor. This optimization significantly reduces GPU memory requirements (by approximately 56%) and increases inference throughput.
The vision tower (SigLIP), multi-modal projector, lm_head, and token embeddings are kept in bf16. See Development Notes below for why.
Deployment
Use with vLLM
This model is tested against vllm/vllm-openai:v0.26.0 on an NVIDIA RTX 5050 (8GB VRAM). Tested launch flags:
docker run \
--gpus=all \
--rm -d \
-p 8080:8080 \
-v /path/to/this/model:/model \
--name gemma3-4b-nvfp4 \
vllm/vllm-openai:v0.26.0-ubuntu2404 \
/model \
--served-model-name syaffers/gemma-3-4b-it-NVFP4 \
--max-model-len 4096 \
--max-num-seqs 8 \
--kv-cache-dtype fp8_e4m3 \
--host 0.0.0.0 \
--port 8080
--max-model-len 4096— the full 131k context in the base model's config is not realistic on 8GB; 4096 leaves enough headroom for KV cache after the ~5.6GB of weights are loaded.--max-num-seqs— this is the biggest lever for VRAM. Start at8; drop to1or2if you see KV-cache-related OOMs at startup (vLLM's own startup log prints how much KV cache memory it actually has available — check that line first).--kv-cache-dtype fp8_e4m3— halves KV cache memory at negligible quality cost, buying back some of the concurrency--max-num-seqscosts you. This is a serving-time flag, not something baked into the checkpoint (kv_cache_schemeinconfig.jsonis unset).
Send a request:
from openai import OpenAI
client = OpenAI(api_key="EMPTY", base_url="http://localhost:8080/v1")
response = client.chat.completions.create(
model="syaffers/gemma-3-4b-it-NVFP4",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image in one sentence."},
{"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}},
],
}],
max_tokens=60,
)
print(response.choices[0].message.content)
Use with transformers
Also loadable directly via transformers + compressed-tensors (no vLLM required), though without NVFP4 GEMM kernels it dequantizes to bf16 on first forward pass, so peak memory briefly needs close to the full bf16 footprint (~8GB) — tight but workable on an 8GB card, more comfortable on CPU or a larger GPU.
import torch
from transformers import Gemma3Processor, Gemma3ForConditionalGeneration
model_id = "syaffers/gemma-3-4b-it-NVFP4"
processor = Gemma3Processor.from_pretrained(model_id)
model = Gemma3ForConditionalGeneration.from_pretrained(model_id, dtype=torch.bfloat16, device_map="cuda:0")
Creation
This model was created with llm-compressor, calibrating on 256 samples from derek-thomas/ScienceQA (a VQA dataset, chosen so calibration exercises both the vision and text paths).
Creation details
Run the following with: `uv run quantize.py --model google/gemma-3-4b-it --output .`# /// script
# requires-python = ">=3.12"
# dependencies = [
# "torch>=2.11.0",
# "transformers",
# "datasets",
# "llmcompressor==0.12.0",
# ]
#
# [tool.uv.sources]
# torch = { index = "pytorch-cu128" }
#
# [[tool.uv.index]]
# name = "pytorch-cu128"
# url = "https://download.pytorch.org/whl/cu128"
# explicit = true
# ///
import argparse
import json
import torch
from datasets import Dataset, load_dataset
from llmcompressor import oneshot
from llmcompressor.modifiers.quantization import QuantizationModifier
from transformers import Gemma3ForConditionalGeneration, Gemma3Processor
def build_calib_dataset(processor, num_samples: int):
"""Pre-tokenized calibration set (see README.md, Creation)."""
ds = load_dataset("derek-thomas/ScienceQA", split="train").filter(
lambda ex: ex["image"] is not None
)
ds = ds.select(range(min(num_samples, len(ds))))
rows = []
for ex in ds:
question = f"{ex['question']}\nChoices: {', '.join(ex['choices'])}"
msgs = [
{
"role": "user",
"content": [{"type": "text", "text": question}, {"type": "image"}],
}
]
prompt = processor.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
enc = processor(text=prompt, images=[ex["image"].convert("RGB")], return_tensors="pt")
rows.append({k: v[0].tolist() for k, v in enc.items()})
return Dataset.from_list(rows)
def patch_vision_tower_ignore_list(output_dir: str):
"""Fix the vision-tower module paths in config.json's `ignore` list.
See README.md, Creation > Post-processing fix, for why this is needed.
"""
config_path = f"{output_dir}/config.json"
with open(config_path) as f:
config = json.load(f)
ignore = config["quantization_config"]["ignore"]
fixed = [
e.replace("model.vision_tower.", "model.vision_tower.vision_model.", 1)
if e.startswith("model.vision_tower.")
and not e.startswith("model.vision_tower.vision_model.")
else e
for e in ignore
]
config["quantization_config"]["ignore"] = fixed
with open(config_path, "w") as f:
json.dump(config, f, indent=2)
print(
f"Patched {sum(a != b for a, b in zip(ignore, fixed))} ignore-list entries "
f"to match the real vision_tower.vision_model.* module path (in config.json)"
)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--model", required=True)
ap.add_argument("--output", required=True)
ap.add_argument("--num-calib-samples", type=int, default=256)
ap.add_argument("--batch-size", type=int, default=1)
ap.add_argument("--max-seq-length", type=int, default=2048)
args = ap.parse_args()
print(f"[1/5] ## Loading model from {args.model}")
model = Gemma3ForConditionalGeneration.from_pretrained(args.model, dtype=torch.bfloat16)
processor = Gemma3Processor.from_pretrained(args.model)
ignore = ["lm_head", r"re:model\.vision_tower.*", r"re:model\.multi_modal_projector.*"]
recipe = [QuantizationModifier(targets="Linear", scheme="NVFP4", ignore=ignore)]
print(f"[2/5] ## Building calibration dataset (scienceqa, n={args.num_calib_samples})")
calib_dataset = build_calib_dataset(processor, args.num_calib_samples)
print(f"[3/5] ## Oneshot NVFP4 quantization (n={args.num_calib_samples})")
oneshot(
model=model,
processor=processor,
dataset=calib_dataset,
recipe=recipe,
batch_size=args.batch_size,
shuffle_calibration_samples=False,
max_seq_length=args.max_seq_length,
num_calibration_samples=args.num_calib_samples,
)
print("[4/5] ## Sanity generation check")
msgs = [
{
"role": "user",
"content": [
{
"type": "text",
"text": "In one sentence, what is the capital of France?",
}
],
}
]
prompt = processor.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
inputs = processor(text=prompt, return_tensors="pt").to(model.device)
out = model.generate(**inputs, max_new_tokens=30, disable_compile=True)
print("===\n", processor.decode(out[0], skip_special_tokens=True), "\n===")
print(f"[5/5] ## Saving to {args.output}")
model.save_pretrained(args.output, save_compressed=True)
processor.save_pretrained(args.output)
patch_vision_tower_ignore_list(args.output)
print("Done.")
if __name__ == "__main__":
main()
Post-processing fix
After export, config.json's quantization_config.ignore list needs one correction: llmcompressor==0.12.0 records vision-tower entries as model.vision_tower.encoder...., but the real module path (and the safetensors tensor names) is vision_tower.vision_model.encoder.... — missing the vision_model segment. This doesn't affect the weights themselves (the vision tower is genuinely plain bf16 either way), but vLLM builds each layer's quant method from this list at model-construction time, before reading any tensor, so a name that doesn't match makes it treat an unquantized bf16 layer as if it were quantized — and it crashes on load with KeyError/AttributeError deep in the SigLIP weight loader. transformers doesn't hit this because it decides per-tensor from what's actually in the checkpoint. Fixed with:
import json
path = "gemma-3-4b-it-NVFP4/config.json"
config = json.load(open(path))
ignore = config["quantization_config"]["ignore"]
config["quantization_config"]["ignore"] = [
e.replace("model.vision_tower.", "model.vision_tower.vision_model.", 1)
if e.startswith("model.vision_tower.") and not e.startswith("model.vision_tower.vision_model.")
else e
for e in ignore
]
json.dump(config, open(path, "w"), indent=2)
Evaluation
Benchmarked with lm-evaluation-harness (lm-eval[api]==0.4.12) against this checkpoint and the unquantized google/gemma-3-4b-it bf16 baseline, both served via vLLM (local-completions backend, --apply_chat_template). Shot counts replicate google/gemma-3-4b-it's own benchmark table for consistency.
Sorted by bf16 score:
| Benchmark | Shots | Metric | bf16 | NVFP4 | Δ |
|---|---|---|---|---|---|
| BoolQ | 0 | acc | 83.73 | 82.45 | −1.28 |
| GSM8K | 8 | exact_match (flexible) | 77.26 | 69.67 | −7.59 |
| PIQA | 0 | acc_norm | 68.66 | 69.10 | +0.44 |
| WinoGrande | 5 | acc | 64.72 | 63.22 | −1.50 |
| ARC-easy | 0 | acc_norm | 63.34 | 61.20 | −2.14 |
| ARC-challenge | 25 | acc_norm | 60.75 | 57.42 | −3.33 |
| MMLU | 5 | acc | 59.58 | 56.13 | −3.45 |
| HellaSwag | 10 | acc_norm | 58.81 | 61.12 | +2.31 |
| TriviaQA | 5 | exact_match | 44.26 | 39.29 | −4.97 |
| DROP | 1 | f1 | 13.70 | 14.28 | +0.58 |
| Natural Questions | 5 | exact_match | 10.86 | 9.78 | −1.08 |
Reading it: simple/short-context tasks (BoolQ, PIQA, ARC-easy) show NVFP4 within ~1-2 points of bf16 — negligible. The real cost shows up on tasks requiring longer reasoning chains — GSM8K (−7.6), MMLU (−3.5), ARC-challenge (−3.3), TriviaQA (−5.0) — where NVFP4 quantization of the language model measurably hurts accuracy. HellaSwag's small NVFP4-ahead result is plausibly noise (single run, no repeated seeds).
Also verified directly (not part of the harness run):
- Text generation: coherent, on-topic completions via both
transformers(bf16 dequant) and vLLM (native NVFP4 GEMM via FlashInfer). - Image understanding: correct image descriptions via both serving paths, using COCO sample images.
Evaluation details
Evaluation details
The following script was used to evaluate the NVFP4/BF16 model served via vLLM's OpenAI-compatible API.
"""Runs the Gemma 3 PT benchmark scheme against a served checkpoint via lm-eval.
Replicates the shot counts from google/gemma-3-4b-it's own README (HellaSwag
10-shot, ARC-e 0-shot, ARC-c 25-shot, MMLU 5-shot, GSM8K 8-shot, etc.) so the
result is comparable across checkpoints. Targets any OpenAI-completions-
compatible endpoint (vLLM, in our case), not a specific checkpoint, so the
same script produces both the NVFP4 and bf16 numbers in this README.
Calls lm-eval's own Python entry point (`lm_eval.simple_evaluate`, the same
function the `lm_eval` CLI calls internally) directly, rather than shelling
out to the CLI, so results come back as a plain dict instead of scraped
terminal output.
Requires a running server, e.g.:
docker run --gpus=all --rm -d -p 8080:8080 \\
-v /path/to/this/model:/model \\
vllm/vllm-openai:v0.26.0-ubuntu2404 /model \\
--served-model-name syaffers/gemma-3-4b-it-NVFP4 \\
--max-model-len 8192 --max-num-seqs 2 --gpu-memory-utilization 0.85 \\
--kv-cache-dtype fp8_e4m3 --host 0.0.0.0 --port 8080
Then run with:
uv run evaluate.py \\
--base-url http://localhost:8080/v1/completions \\
--model syaffers/gemma-3-4b-it-NVFP4 \\
--tokenizer . \\
--output ./eval_results
See README.md ("Evaluation") for the results this produced.
"""
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "lm-eval[api]",
# "transformers",
# ]
# ///
import argparse
import json
from lm_eval import simple_evaluate
from lm_eval.loggers import EvaluationTracker
from lm_eval.utils import handle_non_serializable, make_table
# (tasks, num_fewshot) groups, batched by shot count to minimize server
# restarts.
FEWSHOT_GROUPS = [
(["boolq", "piqa"], 0),
(["drop"], 1),
(["triviaqa", "nq_open", "winogrande", "mmlu"], 5),
(["gsm8k"], 8),
(["hellaswag"], 10),
(["arc_challenge"], 25),
]
def run(tasks: list[str], num_fewshot: int, model_args: str, output: str, limit: int | None) -> dict:
print(f"=== {','.join(tasks)} (n={num_fewshot}) ===")
tracker = EvaluationTracker(output_path=output)
results = simple_evaluate(
model="local-completions",
model_args=model_args,
tasks=tasks,
num_fewshot=num_fewshot,
apply_chat_template=True,
log_samples=True,
evaluation_tracker=tracker,
limit=limit,
)
if results is None:
return {}
# simple_evaluate() only uses evaluation_tracker for config metadata --
# actually persisting results/samples to --output needs these explicit
# calls, same as the lm_eval CLI does after its own simple_evaluate() call.
samples = results.pop("samples")
tracker.save_results_aggregated(results=results, samples=samples)
for task_name in results["configs"]:
tracker.save_results_samples(task_name=task_name, samples=samples[task_name])
print(make_table(results))
if "groups" in results:
print(make_table(results, "groups"))
return results
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--base-url", required=True, help="e.g. http://localhost:8080/v1/completions")
ap.add_argument("--model", required=True, help="served model name")
ap.add_argument("--tokenizer", required=True, help="local checkpoint dir or HF repo id")
ap.add_argument("--output", default="./eval_results")
ap.add_argument(
"--num-concurrent", type=int, default=1,
help="Keep this low (1-2) on consumer GPUs -- lm-eval's multiple-choice "
"tasks request logprobs over the *entire* prompt, which some vLLM "
"versions don't account for in startup memory profiling. This can "
"CUDA OOM mid-run even with modest --max-num-seqs. If that happens, "
"lower this further and/or relaunch the server with a lower "
"--gpu-memory-utilization (0.85 worked for an 8GB/12GB card here).",
)
ap.add_argument("--max-retries", type=int, default=3)
ap.add_argument(
"--limit", type=int, default=None,
help="Cap examples per task (for smoke-testing the script itself, not for "
"real numbers -- results with --limit set are not comparable to the "
"README's).",
)
args = ap.parse_args()
model_args = (
f"model={args.model},base_url={args.base_url},tokenizer={args.tokenizer},"
f"num_concurrent={args.num_concurrent},max_retries={args.max_retries}"
)
all_results = {}
for tasks, num_fewshot in FEWSHOT_GROUPS:
all_results.update(run(tasks, num_fewshot, model_args, args.output, args.limit).get("results", {}))
summary_path = f"{args.output}/summary.json"
with open(summary_path, "w") as f:
json.dump(all_results, f, indent=2, default=handle_non_serializable, ensure_ascii=False)
print(f"Wrote consolidated summary to {summary_path}")
print("ALL_TASKS_COMPLETE")
if __name__ == "__main__":
main()
Development Notes
Development notes
A few things worth recording here for anyone extending this work:- NVIDIA TensorRT Model Optimizer (
nvidia-modelopt) was tried as an alternative to LLM Compressor. It produced a same-size, same-scope checkpoint, but itsexport_hf_checkpoint()writesquant_method: "modelopt"intoconfig.json, which plaintransformersand vLLM don't recognize — loading it silently skips quantization handling entirely (and then OOMs trying to allocate full-size tensors). That export path is meant for TensorRT-LLM or a modelopt-aware runtime, not thetransformers/vLLM stack this checkpoint targets, so we standardized on LLM Compressor's nativecompressed-tensorsoutput instead. - Quantizing the vision tower too was tried, to shrink further (landed around 3.2GB). The resulting checkpoint then failed to load in plain
transformers(AttributeError: 'Linear' object has no attribute 'weight'in SigLIP's_init_weights) and in vLLM (KeyError/AttributeErrorin the SigLIP weight loader) — a current gap in Gemma3-VLM + compressed-tensors support, not something specific to our recipe or calibration. The vision tower is kept unquantized for this reason. - 8GB VRAM (RTX 5050) is tight for calibration, not just serving.
- NVFP4 fake-quantize calibration requires CUDA (asserts
amax.is_cuda), but the 8.1GB bf16 model doesn't fit on an 8GB GPU with any headroom for activations. LLM Compressor'sSequentialPipelinecalibrates one decoder layer at a time, which sidesteps this cleanly — this is the main reason LLM Compressor was easier to work with here than modelopt, which required CPU-offload workarounds viaaccelerateon this hardware. - NVFP4 was served on an RTX 5050 (8GB), bf16 on an RTX 3060 (12GB); both needed
--max-num-seqs 2 --gpu-memory-utilization 0.85— lower than normal serving — because lm-eval's multiple-choice tasks request logprobs over the entire prompt, which vLLM's startup memory profiling doesn't account for and which OOM'd at the normal-serving settings. See Evaluation details for the exact script.
- NVFP4 fake-quantize calibration requires CUDA (asserts
- Downloads last month
- 12