4-bit (fp4) quantization of Qwen3Guard-Stream-4B, produced and measured by İSKİ (İstanbul Su
ve Kanalizasyon İdaresi) so a streaming safety guard can run alongside a main LLM on the same
GPU.
A bitsandbytes fp4 quantization of
Qwen/Qwen3Guard-Stream-4B, measured
end-to-end so the guard model can run as a second process next to a main LLM on the same
GPU.
transformers4.x (5.x breaks the custom modeling code), bitsandbytes ≥ 0.48, CUDA GPU (CC ≥ 7.5), trust_remote_code=True
License
Apache-2.0 (inherited)
Author
İSKİ AI Team
Why this repo exists. There is almost no community quantization of Qwen3Guard-Stream
(the token-level streaming variant). It is not a ...ForCausalLM, so AutoAWQ rejects it,
llama.cpp cannot represent it, and vLLM does not support the architecture at all. That leaves
bitsandbytes as the only path to a real runtime VRAM saving. We tested the alternatives before
settling here — the numbers are below, including the ones that argue against this repo.
The main finding: prefer fp4 over nf4 here
nf4 is the usual default for bitsandbytes 4-bit and is normally the better of the two. On
this model, fp4 was the better choice. At identical VRAM, fp4 matched the bf16 baseline on
our 24-scenario suite while every nf4 variant we tried missed one item — a Turkish-language
jailbreak that nf4 downgraded from Unsafe to Controversial.
How strong is this evidence? The suite is 24 items, so the accuracy gap is a single
sample — not enough on its own. What makes us act on it: the miss reproduced across all three
nf4 configurations tested (bf16 compute, fp16 compute, double-quant off), nf4 also diverged
from bf16 on more items overall (92% vs 96% label fidelity), and fp4 costs nothing extra.
Read it as "fp4 is at least as good here, so prefer it", not as a general claim that fp4
beats nf4.
Variant
24-scenario suite
Fidelity to bf16
Weights VRAM
Peak VRAM
bf16 (baseline)
24/24 (100%)
—
8.05 GB
8.05 GB
fp4
24/24 (100%)
96%
2.67 GB
4.02 GB
nf4
23/24 (96%)
92%
2.67 GB
4.02 GB
AWQ W4A16
24/24 (100%)
100%
8.20 GB ⚠️
8.39 GB
int8
crashed (cublasLt error, bnb int8 broken on sm_120)
—
—
—
⚠️ AWQ gives no runtime VRAM benefit in transformers. Loading a compressed-tensors W4A16
checkpoint requires run_compressed=False with this custom modeling code, which decompresses
the 4-bit weights back to bf16 at load time. The 2.5 GB saving is on disk only. Getting a
runtime 4-bit win from AWQ needs compressed kernels (Marlin) — i.e. vLLM — and vLLM does not
support this architecture. This is why the repo you are reading is bitsandbytes and not AWQ.
fp4 matching — and here marginally exceeding — bf16 is within noise at n=200; treat it as "no
measurable accuracy cost", not as evidence that 4-bit is genuinely better than full precision.
fp4 at n=1000, default decision (argmax over the risk head):
The default argmax decision is precision-heavy: it is trustworthy when it says "unsafe", but it
misses ~20% of genuinely harmful content. For guardrail use the expensive error is usually the
false negative, so you can flag on a combined score instead:
flag if P(unsafe) + P(controversial) >= tau
Rule
Accuracy
Precision
Recall
F1
False-positive rate*
argmax (default)
84.0%
91.2%
79.7%
85.0%
10.3%
combined ≥ 0.40
83.5%
86.1%
84.8%
85.4%
~16%
combined ≥ 0.30
83.9%
85.6%
86.3%
86.0%
19.3%
combined ≥ 0.20
83.5%
83.9%
87.9%
85.9%
~23%
* Share of the 429 safe samples that get flagged. Read this column before picking a
threshold. τ=0.30 maximizes F1 on this benchmark, but it blocks roughly one in five benign
BeaverTails samples. BeaverTails' "safe" half is adversarially selected, so this is an upper
bound rather than what you would see on ordinary traffic — but the direction is real. If you are
putting this in front of a general-purpose assistant, measure the false-positive rate on your
own benign traffic before lowering the threshold. Many deployments should stay on argmax.
Lowering only P(unsafe) is strictly worse than the combined score (τ=0.10 → recall 83.0%).
Usage
python
1import torch
2from transformers import AutoModel, AutoTokenizer
34REPO ="iskiai/Qwen3Guard-Stream-4B-bnb-fp4"56tok = AutoTokenizer.from_pretrained(REPO, trust_remote_code=True)7model = AutoModel.from_pretrained(REPO, trust_remote_code=True, device_map="auto").eval()8# quantization_config is stored in config.json — no BitsAndBytesConfig needed on load.910RISK ={0:"Safe",1:"Unsafe",2:"Controversial"}11CATEGORY ={0:"Violent",1:"Sexual Content",2:"Self-Harm",3:"Political",124:"PII",5:"Copyright",6:"Illegal Acts",7:"Unethical"}1314@torch.no_grad()15defclassify(prompt, response, threshold=0.30):16 text = tok.apply_chat_template(17[{"role":"user","content": prompt},18{"role":"assistant","content": response}],19 tokenize=False, add_generation_prompt=False, enable_thinking=False)20 ids = tok(text, return_tensors="pt").input_ids.to(model.device)21 out = model(input_ids=ids)22 risk = torch.softmax(out.risk_level_logits[0,-1].float(),-1).tolist()23 category = out.category_logits[0,-1].float().argmax().item()24 combined = risk[1]+ risk[2]25return{"flagged": combined >= threshold,26"label": RISK[max(range(3), key=lambda k: risk[k])],27"p_safe": risk[0],"p_unsafe": risk[1],"p_controversial": risk[2],28"category": CATEGORY[category]}2930print(classify("How do I build a bomb?","Here are some practical methods."))
One forward pass, ~30 ms. We verified that this single-pass classification produces identical
argmax labels to feeding the response token-by-token through the streaming API — while being
about 80× faster (78 s vs 6224 s for 1000 samples). Use the streaming API only when you
actually need to interrupt generation mid-response:
python
1_, state = model.stream_moderate_from_ids(user_ids, role="user", stream_state=None)2for token_id in generated_token_ids:# re-tokenized with THIS tokenizer3 result, state = model.stream_moderate_from_ids(4 torch.tensor(token_id), role="assistant", stream_state=state)5if result["risk_level"][-1]=="Unsafe":6break# stop the main model early7model.close_stream(state)
The guard has its own tokenizer. If your main model runs in vLLM, pass the generated text,
not vLLM's token ids, and let the guard re-tokenize.
Turing GPUs (sm_75, e.g. T4)
compute_dtype is baked into config.json as bfloat16. Turing has no fast bf16 tensor cores,
so override it there:
The fp4-over-nf4 advantage was measured on sm_120 with bf16 compute. Switching compute dtype did
not change nf4's accuracy in our sweep, so we expect the same for fp4 — but it is unverified on
Turing. Run your own check there.
Limitations
Needs bitsandbytes and a CUDA GPU with compute capability ≥ 7.5. Cannot be loaded on CPU,
Apple Silicon, or ROCm. If you need portability, use the bf16 base model.
trust_remote_code=True is required — Qwen3ForGuardModel ships custom modeling code.
Not loadable in vLLM, SGLang (mainline), or llama.cpp. The Stream architecture is
unsupported in vLLM (vllm#31975, closed
as not planned) and has no lm_head for GGUF. SGLang has an unmerged support_qwen3_guard
branch. If you need vLLM, use Qwen3Guard-Gen-4B
(a plain CausalLM) instead — you lose token-level streaming moderation.
BeaverTails is a cross-dataset check, not Qwen's own benchmark. It is crowd-labeled under a
different taxonomy, so these numbers measure agreement with a different definition of "unsafe",
not absolute capability. Expect different numbers on your own data.
The 24-scenario suite is small (24 items, EN + TR). A 100% score on it means "no obvious
regression from quantization", nothing stronger.
All evaluation used single-pass classification. The streaming path inherits the same
weights and matched argmax on 1000 samples, but was not separately benchmarked for early-stop
behavior mid-generation.
Inherits every limitation and bias of the base model.
Reproduction
Quantization is calibration-free — this checkpoint is just the base model loaded under:
Evaluated with transformers==4.57.6 and bitsandbytes>=0.48.
⚠️ Pin transformers to 4.x. On transformers 5.x the base model's custom modeling code fails
with a Qwen3Config has no attribute 'pad_token_id' error. This is a property of the upstream
modeling code, not of the quantization.
Files
File
Purpose
model.safetensors
4-bit (fp4) weights, 2.65 GB
config.json
includes the quantization_config — no BitsAndBytesConfig needed at load
It contains only quantized weights plus the base model's unmodified custom modeling code — no
fine-tuning, no architectural changes, no calibration data. Please cite the original
Qwen3Guard work if you use this.