Qwen3Guard-Stream-4B — bitsandbytes 4-bit (fp4)

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.

Model Information

Base model Qwen/Qwen3Guard-Stream-4B
Architecture Qwen3ForGuardModel (36 layers, hidden 2560, no lm_head)
Task Prompt / response safety moderation — Safe · Unsafe · Controversial + 8 risk categories
Quantization bitsandbytes 4-bit, quant_type=fp4, double quantization, compute dtype bf16
Calibration none required (bitsandbytes wraps nn.Linear generically)
Weights on GPU 2.67 GB (bf16 base: 8.05 GB)
Peak VRAM in use 4.02 GB
Latency ~30 ms per single-pass classification
Requires transformers 4.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.

Accuracy

Evaluated against PKU-Alignment/BeaverTails (is_safe labels). Positive class = unsafe.

Head-to-head, same 200 samples:

Config Accuracy Precision Recall F1
fp4 81.5% 90.8% 78.6% 84.3%
bf16 80.5% 90.7% 77.0% 83.3%
AWQ W4A16 80.5% 90.7% 77.0% 83.3%

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):

Metric Value
Accuracy 84.0%
Precision 91.2%
Recall 79.7%
F1 85.0%

Confusion matrix: TP=455, FN=116, FP=44, TN=385 (571 unsafe / 429 safe).

Tuning the operating point

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

import torch
from transformers import AutoModel, AutoTokenizer

REPO = "iskiai/Qwen3Guard-Stream-4B-bnb-fp4"

tok = AutoTokenizer.from_pretrained(REPO, trust_remote_code=True)
model = AutoModel.from_pretrained(REPO, trust_remote_code=True, device_map="auto").eval()
# quantization_config is stored in config.json — no BitsAndBytesConfig needed on load.

RISK = {0: "Safe", 1: "Unsafe", 2: "Controversial"}
CATEGORY = {0: "Violent", 1: "Sexual Content", 2: "Self-Harm", 3: "Political",
            4: "PII", 5: "Copyright", 6: "Illegal Acts", 7: "Unethical"}

@torch.no_grad()
def classify(prompt, response, threshold=0.30):
    text = tok.apply_chat_template(
        [{"role": "user", "content": prompt},
         {"role": "assistant", "content": response}],
        tokenize=False, add_generation_prompt=False, enable_thinking=False)
    ids = tok(text, return_tensors="pt").input_ids.to(model.device)
    out = model(input_ids=ids)
    risk = torch.softmax(out.risk_level_logits[0, -1].float(), -1).tolist()
    category = out.category_logits[0, -1].float().argmax().item()
    combined = risk[1] + risk[2]
    return {"flagged": combined >= threshold,
            "label": RISK[max(range(3), key=lambda k: risk[k])],
            "p_safe": risk[0], "p_unsafe": risk[1], "p_controversial": risk[2],
            "category": CATEGORY[category]}

print(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:

_, state = model.stream_moderate_from_ids(user_ids, role="user", stream_state=None)
for token_id in generated_token_ids:                 # re-tokenized with THIS tokenizer
    result, state = model.stream_moderate_from_ids(
        torch.tensor(token_id), role="assistant", stream_state=state)
    if result["risk_level"][-1] == "Unsafe":
        break                                        # stop the main model early
model.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:

from transformers import BitsAndBytesConfig
model = AutoModel.from_pretrained(
    REPO, trust_remote_code=True, device_map="auto",
    quantization_config=BitsAndBytesConfig(
        load_in_4bit=True, bnb_4bit_quant_type="fp4",
        bnb_4bit_compute_dtype=torch.float16,   # fp16 on Turing
        bnb_4bit_use_double_quant=True))

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 requiredQwen3ForGuardModel 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:

BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="fp4",
                   bnb_4bit_compute_dtype=torch.bfloat16,
                   bnb_4bit_use_double_quant=True)

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
modeling_qwen3_guard.py, configuration_qwen3.py base model's custom modeling code (unmodified)
tokenizer.json, vocab.json, merges.txt, chat_template.jinja, … tokenizer, unchanged from base

License

This repository contains a 4-bit quantization of the Qwen/Qwen3Guard-Stream-4B base model © the Qwen team. Users must obtain and use the base model in accordance with its original license. This checkpoint is distributed under the Apache 2.0 License, inherited from the base model.

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.

Author

Created by İSKİ AI Team · iskiai

Downloads last month
44
Safetensors
Model size
4B params
Tensor type
F32
·
BF16
·
U8
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for iskiai/Qwen3Guard-Stream-4B-bnb-fp4

Finetuned
Qwen/Qwen3-4B
Quantized
(3)
this model