AutoGPTQ developer on retainer: GPTQConfig calibration dataset passed as raw strings instead of tokenized tensors — auto-gptq 0.7.x silently uses character byte values as token IDs — GPTQ Hessian calibrated on wrong distribution — quantized model perplexity 12.8 vs expected 6.2 — 3 weeks — AutoGPTQ developer on monthly retainer

auto-gptq 0.7.x • Python 3.11 • PyTorch 2.3.x • CUDA 12.1 • NVIDIA RTX 4090 • Llama-3-8B • GPTQConfig(bits=4, group_size=128, dataset=calibration_texts) • calibration_texts is a list of raw strings • auto-gptq 0.7.x silently uses character byte values as token IDs • Hessian approximation computed on token IDs 0–127 only • quantization step sizes optimized for wrong distribution • WikiText-2 perplexity 12.8 vs expected 6.2 • developer checks bits, group_size, exllama_config — 3 weeks • fix: tokenize calibration data with tokenizer(); re-quantize; perplexity 6.3 • 1.5h retainer engagement

Westfield GPTQ Inference Ltd. needed to deploy a Llama-3-8B model on an NVIDIA RTX 4090 with 24 GB VRAM. They used AutoGPTQ to quantize to 4-bit with GPTQConfig(bits=4, group_size=128) and saved the quantized model to disk. The quantization ran without errors. Inference worked. But when they benchmarked the quantized model against the base model on their evaluation suite, the quantized model’s perplexity on WikiText-2 was 12.8 — far worse than the 6.2–6.5 range expected from a well-calibrated 4-bit GPTQ model. Generated text quality was visibly degraded: more repetition, shorter coherent spans, more frequent off-topic completions. The developer spent three weeks adjusting bits (to 8), group_size (to 64), and exllama_config combinations before engaging an AutoGPTQ retainer developer.

The quantization configuration at the time of the engagement:

# quantize.py — Westfield GPTQ Inference Ltd.
# auto-gptq 0.7.x, Python 3.11, PyTorch 2.3.x, CUDA 12.1, RTX 4090

from transformers import AutoTokenizer, AutoModelForCausalLM
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig

# Calibration data — PROBLEM: raw strings, not tokenized tensors
calibration_texts = [
    "The history of machine learning began in the 1950s when researchers...",
    "Neural networks are computational models inspired by the structure of...",
    "Large language models are trained on vast corpora of text data to...",
    # ... 125 more strings
]
# type(calibration_texts[0]) == str          (WRONG — should be torch.Tensor)
# calibration_texts[0][0] == 'T'             (first char, not first token)
# ord('T') == 84                             (byte value used as token ID)
# Llama-3 vocab token 84 == 'S' (a common subword token)
# All calibration "tokens" will be in range 0-127 (ASCII byte values)
# Hessian computed over first 128 vocab entries only

quantize_config = BaseQuantizeConfig(
    bits=4,
    group_size=128,
    desc_act=False,
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Meta-Llama-3-8B",
    device_map="auto",
    torch_dtype="auto",
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B")

# No error raised — auto-gptq 0.7.x accepts raw strings silently
# Iterates each string character by character
# Uses ord(char) as token ID for Hessian accumulation
# Hessian calibration runs to completion — no indication of wrong inputs
model.quantize(calibration_texts)

model.save_quantized("llama-3-8b-gptq-4bit", use_safetensors=True)
# Quantization complete: no errors, no warnings
# Model saved: 4.3 GB (vs 16 GB for bf16 base model)
# But: WikiText-2 perplexity = 12.8 (vs 6.2 expected for properly calibrated 4-bit)

Why the GPTQ algorithm requires tokenized calibration tensors

The GPTQ algorithm (Frantar et al., 2022) quantizes each weight matrix by minimizing the second-order approximation of the output error, measured using the Hessian of the layer’s output with respect to the weights. This Hessian is computed empirically by running the calibration dataset through the model and accumulating the outer products of the input activations at each layer. The quality of the Hessian approximation depends entirely on the calibration data representing the distribution of inputs the model will actually see during inference.

When raw strings are passed to AutoGPTQ’s calibration pipeline, auto-gptq 0.7.x processes them without a type check. The library’s data pipeline expects each calibration sample to be a torch.Tensor of integer token IDs (shape [seq_len]) or a list[int]. When a Python string is encountered, Python’s iteration protocol applies: iterating a string yields individual characters. Each character is implicitly converted to an integer via its byte value (0–127 for ASCII text). The GPTQ calibration then runs forward passes through the model using these byte-value integers as token IDs.

For a Llama-3-8B model with a 128,000-token vocabulary, the byte values 0–127 map to the very first entries in the vocabulary embedding table — mostly special tokens, punctuation, and a handful of low-frequency subword fragments. The Hessian is computed for an input distribution concentrated entirely in this tiny slice of the vocabulary space. The weight sensitivity statistics accumulated during calibration are meaningless for the actual deployment distribution, which spans the full vocabulary. The resulting quantization step sizes are wrong for the actual inputs. At inference time, the quantized weights introduce quantization error far above the theoretical minimum, driving perplexity from 6.2 to 12.8.

# Diagnosing the calibration data type issue

# Check #1: type and value inspection
print(f"Type: {type(calibration_texts[0])}")
# Type: <class 'str'>  ← WRONG — should be torch.Tensor or list[int]

# What auto-gptq actually sees when iterating a string:
sample = calibration_texts[0]  # "The history of machine learning..."
pseudo_token_ids = [ord(c) for c in sample[:20]]
print(f"Pseudo token IDs from first 20 chars: {pseudo_token_ids}")
# [84, 104, 101, 32, 104, 105, 115, 116, 111, 114, 121, 32, 111, 102,
#  32, 109, 97, 99, 104, 105]
# Range: 32-122 (printable ASCII)
# Expected range for English text tokens: 0-128000 (Llama-3 vocab)
# Actual range used: 32-122 (ASCII printable characters)

# Check #2: tokenize a sample and compare
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B")
proper_tokens = tokenizer(calibration_texts[0], return_tensors='pt').input_ids[0]
print(f"Proper token IDs (first 10): {proper_tokens[:10].tolist()}")
# [128000, 791, 3925, 315, 5780, 6975, 3940, 304, 279, 220]
# Range: 0-128000; mean ~15000; covers full vocabulary distribution

# Check #3: assert to catch the bug in future runs
for i, sample in enumerate(calibration_texts):
    assert not isinstance(sample, str), (
        f"Calibration sample {i} is a raw string, not a tokenized tensor. "
        f"Use: tokenizer(text, return_tensors='pt').input_ids[0]"
    )
# AssertionError: Calibration sample 0 is a raw string, not a tokenized tensor.

The fix: tokenize calibration data before passing to GPTQConfig

The fix tokenizes all calibration texts using the model’s tokenizer before passing them to BaseQuantizeConfig. Each calibration sample becomes a torch.Tensor of token IDs covering the full vocabulary distribution. The GPTQ algorithm’s Hessian approximation is then computed on activations from real text inputs, producing quantization step sizes optimal for the actual deployment distribution.

# quantize.py — FIXED (Westfield GPTQ Inference Ltd.)
# auto-gptq 0.7.x, Python 3.11, PyTorch 2.3.x, CUDA 12.1, RTX 4090

from transformers import AutoTokenizer, AutoModelForCausalLM
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
import torch

raw_calibration_texts = [
    "The history of machine learning began in the 1950s when researchers...",
    "Neural networks are computational models inspired by the structure of...",
    "Large language models are trained on vast corpora of text data to...",
    # ... 125 more strings
]

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B")

# FIXED: tokenize calibration data before passing to quantization
calibration_data = [
    tokenizer(
        text,
        return_tensors="pt",
        truncation=True,
        max_length=2048,  # Match model's typical inference context length
    ).input_ids[0]
    for text in raw_calibration_texts
]

# Validate tokenized calibration data
assert all(isinstance(t, torch.Tensor) for t in calibration_data), \
    "All calibration samples must be tensors"
assert all(t.dtype == torch.long for t in calibration_data), \
    "Calibration tensors must be integer type"
max_token_id = max(t.max().item() for t in calibration_data)
assert max_token_id > 500, (
    f"Max token ID = {max_token_id} — looks like byte values, not vocabulary IDs. "
    f"Expected max ~{tokenizer.vocab_size}"
)

print(f"Calibration data prepared:")
print(f"  Count: {len(calibration_data)} samples")
print(f"  Shape: {calibration_data[0].shape}")
print(f"  Token ID range: 0 to {max_token_id}")
print(f"  Expected vocab size: {tokenizer.vocab_size}")
# Calibration data prepared:
#   Count: 128 samples
#   Shape: torch.Size([2048])
#   Token ID range: 0 to 127996
#   Expected vocab size: 128000  ✓

quantize_config = BaseQuantizeConfig(
    bits=4,
    group_size=128,
    desc_act=False,
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Meta-Llama-3-8B",
    device_map="auto",
    torch_dtype="auto",
)

model.quantize(calibration_data)

model.save_quantized("llama-3-8b-gptq-4bit-fixed", use_safetensors=True)
# Quantization complete: 4.3 GB
# WikiText-2 perplexity = 6.3 (matches expected 6.2)  ✓

Validation

# Validation log — November 2026
# auto-gptq 0.7.x, Llama-3-8B, bits=4, group_size=128, RTX 4090

# ---- Before fix: raw string calibration ----
# type(calibration_data[0]) = str
# Pseudo token IDs range: 32-122 (ASCII byte values)
# Hessian calibration: 128 samples × ~500 char-byte tokens each
# WikiText-2 perplexity: 12.8
# Coherent text span (chars before repetition): ~180
# Benchmark eval accuracy on internal test set: 61.2% (vs 77.4% base model)

# ---- After fix: tokenized tensor calibration ----
# type(calibration_data[0]) = torch.Tensor
# Token ID range: 0 to 127,996 (full vocabulary)
# Hessian calibration: 128 samples × 2,048 real tokens each
# WikiText-2 perplexity: 6.3 (matches expected 6.2 for 4-bit, group=128)
# Coherent text span: >500 chars (comparable to base model)
# Benchmark eval accuracy: 76.8% (within 0.6% of base model 77.4%)

# Perplexity reduction: 12.8 → 6.3 (51% improvement)                 ✓
# Calibration tensor type assertion passes                             ✓
# Max token ID > 500 assertion passes                                  ✓
# Model size: 4.3 GB (same — same bits/group_size, quantization quality improved)

# ---- Hour breakdown ----
# Triage:     0.5h — perplexity 12.8 vs expected 6.2; inspect calibration data;
#             type(calibration_data[0]) == str; iterate string → byte values 32-122
# Root cause: 0.25h — confirmed: auto-gptq 0.7.x accepts raw strings silently;
#             Hessian computed on ASCII byte distribution not vocabulary distribution
# Fix:        0.5h — tokenize calibration data; add type and value range assertions
# Validation: 0.25h — re-quantize; perplexity 6.3; assertions pass; model quality restored
# Total:      1.5h

# Retainer engagement cost: 1.5h (estimated $150–225 at mid-level rates)

Related retainer posts


Track retainer hours without a client portal

HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.

See pricing →