~/satyajit

Qwen3.8-2B-Distill: the filter wrote most of the headline

mdjsonmcp

2026-08-26 · 19 min · qwen · distillation · quantization · gguf · evaluation · edge

empero-ai/Qwen3.8-2B-Distill-GGUF is a full-parameter distillation of Qwen3.8 2.4T A95B into the smallest member of the Qwen3.5 family, quantized for llama.cpp. It has 155,055 downloads and 93 likes eleven days after upload. The pitch is compact and appealing: the reasoning curriculum of a 2.4-trillion-parameter teacher, MMLU 0.283 → 0.548, GSM8K 0.330 → 0.640, 262k context, function calling, 1.3 GB on a phone.

I went and read the files. Not the card — the five GGUF headers, the safetensors index, the config.json, the chat template, llama.cpp's memory allocator, the lm-evaluation-harness task YAMLs that produced those numbers, and the training_args.bin that Empero shipped alongside the weights and probably did not mean to publish as evidence.

Two things came out of it. The distillation is real and cheap, and it did something worth having. And the benchmark table is a much weaker claim than it looks, in a way the parent model card itself discloses and the GGUF card does not reprint.

What it isQwen3.5-2B, full-parameter SFT on ~30,000 Qwen3.8 teacher traces, converted to GGUF
Repofive quants, nothing under 1 GB — Q4_K_M is 1,312,164,224 bytes
Real parameter count2,274,069,824 in the safetensors; 1,942,653,248 in the GGUF; 1,373,265,728 in the transformer that does the work
Visionthe base is a VLM. The 331M-parameter ViT is in the download and absent from every GGUF
Advertised context262,144 — and the GGUF header agrees, qwen35.context_length = 262144
Context it was trained at8,192max_length in the shipped training_args.bin
KV cache at 262,1443.221 GB, or 2.45× the weights it serves
Training run1 epoch · lr 3e-5 cosine · 5 warmup steps · paged_adamw_8bit · one GPU · seed 42
The headline metriclm-eval flexible-extract, which takes the last number in the output
The column not reprintedGSM8K strict-match: 0.545 → 0.640, a gain of +0.095, not +0.310
Same recipe at 4B / 9BGSM8K −0.065 and −0.015. The distillation makes the larger siblings worse
LicenseApache-2.0, inherited. Genuinely open
empero-ai/Qwen3.8-2B-Distill-GGUFhugging face · snapshot 2026-09-08
repo size
10.35 GB
license
apache-2.0
downloads
533.5K
likes
129
files
8

What is actually in the repo

Start with the byte sizes, because they are the one thing nobody can spin. The Hugging Face blob listing gives exact lengths, and each file's own GGUF tensor table gives the breakdown.

A horizontal stacked bar chart of the five GGUF files in the repository — Q4_K_M, Q5_K_M, Q6_K, Q8_0 and BF16 — each split into the token embedding table, other tensors held at the file's top precision, tensors at the nominal quant, and the tokenizer header. A dashed red line at 1 GB sits to the left of every bar.
Every file in empero-ai/Qwen3.8-2B-Distill-GGUF, decomposed by summing its own tensor table. The dashed line is the circulating ‘~1 GB on a phone’ claim; the smallest file in the repo is 1.312 GB. (Rendered from the HF blob listing and the GGUF headers.)

There is no Q3, no Q2, no IQ quant. The smallest file on offer is Q4_K_M at 1.312 GB — 1.222 GiB if you prefer binary — and it is the one the card recommends. So the "~1 GB" figure is not a rounding of anything in this repository. It has no file behind it.

The reason is structural, and it is the most interesting thing about the ladder. I decoded the tensor table of each file and summed the ggml block sizes; for Q4_K_M the total lands on 1,312,164,210 bytes against a 1,312,164,224-byte file, a 14-byte alignment remainder. Inside that total, the Q6_K bytes (657,162,240) outweigh the Q4_K bytes (641,802,240). Half the byte volume of a file called Q4_K_M is at six bits, and its true rate is 5.404 bits per weight, not 4.5.

where a “2B” goes · 2,274,069,824 params on disk, 1,942,653,248 in the GGUF71.1% smaller after conversion
Two bars on the same byte scale. The safetensors download is 4.548 GB, of which the vision tower is 662.8 MB. The Q4_K_M GGUF is 1.312 GB and contains no vision tensors at all. The currently selected group is token_embd (tied).model.safetensors · bf164.548 GB60%22%15%Qwen3.8-2B-Q4_K_M.gguf1.312 GB64%32%663 MB of vision tower, dropped in conversionboth bars on one byte scale · segment widths are real bytes, not illustrative
parameters
508,559,360
in the safetensors
1017.1 MB
in the Q4_K_M GGUF
417.2 MB
effective precision
6.56 bits/weight

248,320 tokens × 2,048 dims. tie_word_embeddings is true, so this matrix is also the output projection — which is why llama.cpp quantizes it to Q6_K even in the Q4_K_M build. 6.56 bits per weight against 4.5 for everything else, and 31.8% of the file you download.

“2B” is doing a lot of work as a label. The safetensors file holds 2,274,069,824 parameters. Take out the vision tower the text fine-tune never touched and you have 1.943B, which is what the GGUF contains, to the parameter. Take out the tied embedding table and the multi-token-prediction head and the transformer that actually answers your question is 1.373B.

That last subtraction is not pedantry, because the embedding is where the quantization argument goes to die. Qwen3.5’s vocabulary is 248,320 tokens — it covers 201 languages and carries vision and tool sentinels — so at hidden size 2,048 the table alone is 508.6M parameters, more than a quarter of the model. And tie_word_embeddings: true means it doubles as the output projection, which llama.cpp will not quantize to four bits. The result is a 417 MB Q6_K floor under every K-quant in the repo. Half the byte volume of the “4-bit” file is at six bits, its true rate is 5.40 bits per weight, and that is the arithmetic reason the smallest file on offer is 1.312 GB rather than something starting with a zero.

token_embd.weight alone is 417,177,600 bytes — 31.8% of the download — and llama.cpp holds it at Q6_K. That is not a quirk of this publisher's recipe. Qwen3.5's vocabulary is 248,320 tokens because it covers 201 languages plus vision and tool sentinels, and config.json sets tie_word_embeddings: true, so that matrix is also the output projection. llama.cpp will not take the output projection to four bits. The floor is 417 MB before a single transformer weight is quantized, and it is why the entire K-quant ladder is compressed into 1.31–1.61 GB.

The other surprise in that ledger is what is missing. The parent is Qwen3_5ForConditionalGeneration — the base is a vision-language model, and the safetensors carries a 24-layer ViT at model.visual.*, 331,416,576 parameters. The GGUF parameter count is 1,942,653,248, which is exactly the safetensors minus that tower, to the parameter. No GGUF in the repo contains a single v.* or mm.* tensor and there is no mmproj file. 663 MB of the bf16 download does nothing in llama.cpp. The parent card is upfront that the fine-tune is text-only; the GGUF card does not mention vision at all.

The metric is doing the work

Here is the table as the GGUF card prints it:

TaskQwen3.5-2B (base)Qwen3.8-2BΔ
mmlu (CoT, 57 subjects)0.2830.548+0.265
gsm8k_cot0.3300.640+0.310

And here is the parent card's version of the same table, which has twice as many rows:

TaskMetricbasestudentΔ
gsm8k_cotexact_match (flexible)0.3300.640+0.310
gsm8k_cotexact_match (strict)0.5450.640+0.095
mmlu (CoT)acc (flexible-extract)0.2830.548+0.265
mmlu (CoT)acc (strict-match)0.0040.225+0.221

Look at the second row. Under lm-eval's strict-match filter the base model already solves 54.5% of GSM8K, and the distilled student reaches 64.0%. The gain is +0.095. The headline +0.310 exists because the base scores 0.330 under the other filter — 21.5 points lower than under the strict one.

If your intuition says a "flexible" filter should never score lower than a "strict" one, that intuition is wrong, and the reason is one line of YAML.

lm_eval · gsm8k_cot · both filters, run live · gold = 8strict 1 · flexible 0
Olivia started with 23 dollars. Five bagels at 3 dollars each cost 5 * 3 = 15 dollars. So 23 - 15 = 8. The answer is 8.
Checking that: 5 bagels at 3 dollars is 15 dollars of spending, and 23 - 15 leaves 8, so she keeps roughly a third of the 23 she walked in with.
Two filter pipelines applied to the same generation. strict-match captures "8" and scores 1; flexible-extract captures "23" and scores 0. The gold answer is 8.strict-matchThe answer is (\-?[0-9\.\,]+).group_select 0 · first8vs 81 correctflexible-extract(-?[$0-9.,]{2,})|(-?[0-9]+)group_select -1 · last23vs 80 wrong

The model is right, says so in the required sentence, and then keeps talking. strict-match takes the first “The answer is N”; flexible-extract takes the last number in the whole generation, which by then is a number from the sanity check.

The word “flexible” makes it sound like a relaxed version of strict-match — same rule, more forgiving. It is not. They are two unrelated extraction rules that happen to be reported side by side. strict-match hunts for one specific sentence and takes the first one it finds anywhere in the generation. flexible-extract ignores sentences entirely and takes the last number-shaped substring in the output, because the task YAML sets group_select: -1.

So neither dominates the other, and which one flatters a model depends on how the model ends its answer rather than on whether it got the question right. A model that solves the problem and then talks for another sentence loses under flexible-extract. A model that solves the problem and never writes the sentence loses under strict-match. Distillation on chat traces fixes both habits at once — which is worth real money in production, and is not the same thing as having learned to reason.

From lm_eval/tasks/gsm8k/gsm8k-cot.yaml, verbatim:

filter_list:
- filter:
  - function: regex
    regex_pattern: The answer is (\-?[0-9\.\,]+).
  - function: take_first
  name: strict-match
- filter:
  - function: regex
    group_select: -1
    regex_pattern: (-?[$0-9.,]{2,})|(-?[0-9]+)
  - function: take_first
  name: flexible-extract

group_select: -1. lm-eval's RegexFilter does match = self.regex.findall(resp) then match = match[self.group_select], so -1 means the last match anywhere in the generation. flexible-extract is not a relaxed strict-match; it is an unrelated rule that says "the answer is the last number-shaped substring in the output." (take_first is a separate filter that picks the first of N sampled responses. With repeats: 1 it does nothing, and it has no bearing on which match wins.)

So a model that solves the problem, states the answer in the required sentence, and then adds one more sentence of sanity-checking will be scored on whatever number it happened to mention last. That is exactly the shape of an instruction-tuned base model that has not been taught to shut up. And a model that solves the problem but never writes "The answer is" scores zero under strict-match no matter how right it was. Neither filter dominates; which one flatters a model depends on how it ends its answer.

Qwen3.5-2B → Qwen3.8-2B · gsm8k_cotheadline Δ +0.310 · 69% of it is extraction
·
A waterfall for Qwen3.5-2B to Qwen3.8-2B on gsm8k_cot. The base scores 0.330 under flexible-extract. The strict-match filter moves up by 0.095, and the extraction gap closing adds 0.215, reaching 0.640.0.000.250.500.751.000.330base, flexible-extract+0.095Δ under strict-match+0.215Δ from the extraction gap0.640student, flexible-extractΔflexible +0.310 = Δstrict +0.095 + Δgap +0.215 — an identity, not a fit
base strict-match
0.545
base extraction gap
−0.215
student extraction gap
+0.000
headline Δ
+0.310

The base already solves 54.5% of GSM8K by the harness's own strict rule. flexible-extract scores that same model at 33.0% because it grabs the last number in the output. Two thirds of the advertised +0.310 is that gap closing — the student stops talking once it has answered.

The split is arithmetic, not interpretation. Define each model’s extraction gap as flexible-extract minus strict-match on the same generations. Then the headline delta is the strict-filter delta plus the change in that gap, exactly, with nothing left over. The equation under the chart is that identity evaluated for the current selection.

What the two terms mean is where judgement comes in, and it is not the same in every cell. Step through the six of them. On gsm8k_cot the 2B is the only one of the three scales with a positive headline — and it is also the only one whose base model had a large extraction gap to give back. At 4B and 9B, where the base already stopped at its answer, the same recipe and the same teacher make the model worse. That is the shape of a change to output habits, not to capability.

The decomposition is an identity, not an interpretation. Define each model's extraction gap as flexible minus strict on the same generations. Then

Δflex  =  Δstrict  +  (gapstudentgapbase)\Delta_{\text{flex}} \;=\; \Delta_{\text{strict}} \;+\; \big(\text{gap}_{\text{student}} - \text{gap}_{\text{base}}\big)

with nothing left over. For GSM8K at 2B: +0.310=+0.095+0.215+0.310 = +0.095 + 0.215. 69% of the advertised GSM8K gain is the base model's extraction gap closing — the student stops talking once it has answered, so both filters land on the same token and its two columns agree to three decimals.

The siblings settle it

Empero released the same recipe at three scales on the same day, and published all three tables. That is a natural experiment they handed us for free.

Two grouped bar charts, gsm8k_cot and MMLU CoT, each showing base and distilled scores under strict-match and flexible-extract for the 2B, 4B and 9B models, with the delta printed above each pair. GSM8K deltas are positive only at 2B and negative at 4B and 9B; MMLU deltas are positive everywhere.
The same teacher, the same curriculum, three student sizes. GSM8K improves only where the base had an extraction gap to give back. (Rendered from the empero-ai Qwen3.8-2B / -4B / -9B model cards.)

The base models' GSM8K extraction gaps, flexible minus strict:

Basegapreported Δ after distillation
Qwen3.5-2B−0.215+0.310
Qwen3.5-4B0.000−0.065
Qwen3.5-9B+0.010−0.015

The only scale with a headline GSM8K gain is the only scale whose base had a large extraction penalty to recover. At 4B and 9B, where the base already stopped at its answer, the identical recipe with the identical teacher makes the student worse — 6.5 and 2.5 points worse under a fixed extraction rule. If what was transferred were reasoning, it would not evaporate at 4B, where the student has twice the capacity to hold it.

MMLU is a different and more favourable case, and I want to be careful here because the sceptical reading does not extend cleanly. MMLU CoT improves at all three scales under both filters. But the base's strict-match is 0.004. That is not a knowledge measurement — the mmlu_flan_cot_zeroshot strict filter is four lookbehinds demanding a literal "The answer is (X)" (or one of three near-identical variants) as the last thing on its line, zero-shot, with no exemplar ever shown. The base essentially never writes it. So on MMLU, strict-match cannot see the base at all, and neither column isolates reasoning. The +0.265 is real as a harness result. How much of it is knowledge and how much is the student learning where to put the answer is a question these four numbers cannot answer, and the control that would answer it — rescoring the base few-shot, or with a format-forcing instruction — is not on the card.

The one hint we have points the same way as everything else: gsm8k_cot is an 8-shot task whose YAML bakes in eight exemplars that all end "The answer is N." MMLU CoT is zero-shot. The task where the base is shown the format eight times yields +0.095. The task where it is never shown the format yields +0.265.

What the eval report leaves out

The card says "Measured with lm-evaluation-harness, HF backend, identical settings for base and student." That is more than most publishers write down. It is still not enough to reproduce a number, and the gaps are specific:

That last point is worth pulling out, because it reframes the base model entirely. Qwen's own card for Qwen/Qwen3.5-2B reports MMLU-Redux 69.2 in non-thinking mode and 79.6 in thinking mode, and MMLU-Pro 55.3 / 66.5. Whatever "0.283" measures about Qwen3.5-2B, it is not that model's knowledge; it is that model's score under one zero-shot generative harness whose extractor takes the last parenthesised capital letter it can find. The distilled student's 0.548 sits fourteen points below the base's own published MMLU-Redux.

The training run, from the file they shipped

training_args.bin is in the parent repo — 5,777 bytes, a torch.save of TRL's SFTConfig. Unpickle it and the entire run is there:

learning_rate      = 3e-05        lr_scheduler_type = cosine
num_train_epochs   = 1            warmup_steps      = 5
per_device_train_batch_size = 8   gradient_accumulation_steps = 8
max_length         = 8192         truncation_mode   = keep_start
packing            = True         packing_strategy  = bfd
assistant_only_loss = False       loss_type         = nll
optim              = paged_adamw_8bit
bf16 = True   gradient_checkpointing = True   seed = 42
_n_gpu = 1    distributed_type = NO    fsdp = None    deepspeed = None

Four things fall out of that block.

It is a genuine full fine-tune. paged_adamw_8bit with gradient checkpointing on a single GPU is precisely what you reach for when you are fitting every parameter of a 2.27B bf16 model plus optimizer state onto one card. Nobody pages an 8-bit optimizer to train an adapter. The repo has one 4.55 GB model.safetensors and no adapter_config.json. The "full-parameter SFT, not LoRA" claim survives contact with the artifacts.

It is about a hundred optimizer steps. Effective batch is 8 × 8 = 64 sequences of 8,192 packed tokens, one epoch over ~30,000 traces. If a CoT trace averages 1,000–3,000 tokens, that is 30–90M tokens, 3,700–11,000 packed bins, and roughly 60–170 optimizer steps. warmup_steps: 5 and save_steps: 25 with save_total_limit: 3 are consistent with a run in exactly that range. Whatever "the full reasoning curriculum of a 2.4T model" means, this is a short, cheap style transfer on one GPU — which is a perfectly good thing to build, and is not what the phrase suggests.

The loss is on everything. assistant_only_loss: False with packing: True: sequences are bin-packed into 8,192-token bins and trained with plain next-token NLL across the whole bin, prompts included. A meaningful share of the gradient is spent learning to predict user turns.

max_length: 8192. Every token this model ever saw during its reasoning fine-tune lived in an 8,192-token window, with anything longer truncated from the end. The card advertises 262,144. That is 3.1% coverage, and the card is careful to say the context is inherited — but "262k context with real reasoning" is a claim about two things that were never trained together.

The context is real, and it is not free

To its credit, the 262k claim survives the check I expected it to fail. The GGUF header does not quietly say 32768:

qwen35.context_length         = 262144
qwen35.block_count            = 25
qwen35.attention.head_count   = 8
qwen35.attention.head_count_kv = 2
qwen35.attention.key_length   = 256
qwen35.rope.freq_base         = 10000000.0
qwen35.rope.dimension_count   = 64
qwen35.full_attention_interval = 4

config.json agrees: max_position_embeddings: 262144, rope_theta: 1e7. Function calling is likewise real — the embedded chat template renders a <tools> block and a <tool_call><function=…> grammar, and the tokenizer carries <tool_call> and </tool_call> as dedicated tokens at ids 248058–248059. Both are inherited wholesale from the base, and neither was evaluated by Empero. The base's own card reports BFCL-V4 43.6 and TAU2-Bench 48.8; those are Qwen's numbers for the base, and they are the only tool-use numbers that exist for this model.

What the card does not put next to 262,144 is the memory bill.

what one llama.cpp process holds · Q4_K_M · 98k ctx · KV f162.86 GB against a 4.29 GB budget — fits
··
A 8 GB device with 50% usable gives a 4.29 GB budget. Q4_K_M weights take 1.31 GB, a 98k-token f16 KV cache takes 1.21 GB, the Gated DeltaNet state 20 MB and runtime overhead 320 MB, for 2.86 GB total — which fits inside the budget.physical RAM · 8 GBbudget 4.29 GB (50%)what llama.cpp allocates2.86 GB totalweights 1.31 GBKV cache 1.21 GBdeltanet state 20 MBruntime 320 MB (assumed)
A strip from zero to 262,144 tokens. The supervised fine-tune used a max_length of 8,192, which is 3.1% of the strip. The cursor sits at 98,304 tokens.8,192 — every token the reasoning fine-tune ever saw (3.1%)262,144 — the advertised windowyou asked for 98,304 · 1.21 GB of KV
weights on disk
1.31 GB
KV at 98k
1.21 GB
KV per token
12.000 KiB
headroom left
1.43 GB
longest context this configuration can hold
215,055 tokens — 82.0% of the advertised 262,144 (largest round stop: 196,608)

The model card sells a 1.312 GB download and a 262,144-token window as if they were independent facts. They are not. llama.cpp sizes the attention cache to whatever you pass --ctx-size and allocates all of it when the model loads, so the context length you ask for is a memory decision you make before the first token comes out.

The per-token cost is fixed by the config: num_key_value_heads: 2, head_dim: 256, and six full-attention layers among twenty-four — 2 × 2 × 256 × 6 = 6,144 values per token, or 12 KiB at f16. Multiply by 262,144 and the cache is 3.22 GB, two and a half times the weights it serves.

Push the context slider to the right on an 8 GB phone and watch the red segment eat the budget. Then set KV to q8_0 and watch it halve — that flag, not the quant you picked, is the one that decides whether long context is reachable on a handset. The eighteen Gated DeltaNet layers are the reason any of this is survivable: they keep a fixed 20.2 MB of recurrent state no matter how long the conversation gets. Had all twenty-four layers been full attention, 262k would want 12.9 GB of cache and no phone would be in this conversation at all.

The arithmetic is fixed by the config and it is short. num_key_value_heads: 2, head_dim: 256, and layer_types is eighteen linear_attention layers to six full_attention — so per token the cache holds 2 (K and V) × 2 heads × 256 dims × 6 layers = 6,144 values, or 12,288 bytes at f16. I checked that llama.cpp really does skip the recurrent layers rather than allocating for all twenty-four; in src/llama-model.cpp the hybrid memory filter for LLM_ARCH_QWEN35 is

filter_attn = [&](uint32_t il) {
    return il < hparams.n_layer() && !hparams.is_recr(il);
};

which also excludes blk.24, the MTP block, since its index is not less than n_layer. The cache is sized to cparams.n_ctx_seq — the full --ctx-size — and allocated when the model loads, not as the conversation grows.

So at the advertised context: 262,144 × 12,288 = 3,221,225,472 bytes. The KV cache is 2.45× the Q4_K_M weights, and the two are equal at 106,784 tokens.

A log-x line chart of memory against context length from 2k to 262k tokens, showing KV cache curves for f16, q8_0 and q4_0 against a flat line for the 1.312 GB Q4_K_M weights, plus a dashed curve for a hypothetical all-attention model, with markers at the 8,192-token training window and the 262,144-token advertised context.
KV bytes per token derived from config.json; the allocation policy read out of llama.cpp's hybrid memory filter for LLM_ARCH_QWEN35. The f16 cache overtakes the whole model file at 107k tokens. (Rendered from the shipped config and llama.cpp source.)

The eighteen Gated DeltaNet layers are the reason this is survivable at all. They keep a fixed recurrent state — 16 heads × 128 × 128 in f32 plus a 6,144-channel convolution window, 20.2 MB total — that does not grow with context. Had all twenty-four layers been full attention, 262k would want 12.9 GB of cache and no handset would be in the conversation. That saving is Qwen's architecture, not the distillation's, and the card that advertises 262k credits it to nobody.

The practical lever is not the quant, it is --cache-type-k. Going to q8_0 cuts the cache to 1.71 GB at full context; q4_0 takes it to 0.91 GB — below the weights again. Rotate-then-quantize is the current state of the art for doing that without wrecking the attention scores, and on a model with this weights-to-cache ratio it matters more than the difference between Q4_K_M and Q6_K.

The reasoning is off by default

One last thing, and it is the kind that only shows up if you open the file. The GGUF card says:

The model is a reasoning model: every answer opens with a <think> block, so allow a generous -n and strip the <think>...</think> span for end users. Use the built-in chat template (-cnv).

The built-in chat template ends like this:

{%- if add_generation_prompt %}
    {{- '<|im_start|>assistant\n' }}
    {%- if enable_thinking is defined and enable_thinking is true %}
        {{- '<think>\n' }}
    {%- else %}
        {{- '<think>\n\n</think>\n\n' }}
    {%- endif %}
{%- endif %}

Thinking is off unless the caller passes enable_thinking=true. The default branch pre-fills a closed, empty think block, which is Qwen3.5's way of switching reasoning off. And chat_template.jinja in the distill repo is byte-identical to the base's — 7,755 bytes, same MD5. Diff the two config.json files and the only substantive change in the entire repository is eos_token_id moving from 248044 (<|endoftext|>) to 248046 (<|im_end|>), which is a correct and sensible fix for a chat SFT.

Which means the artifact does not agree with its own instructions. A model sold on distilled chain-of-thought ships a template whose default suppresses it, and a one-line change to that template would have fixed it. It also raises a question about the evals that the card cannot answer: lm-eval only applies a chat template when you pass --apply_chat_template, and passing enable_thinking=true through it needs --chat_template_args. The card says neither. If the template was applied without that flag, both models were scored with thinking disabled.

The ledger

What is genuinely here. A real full-parameter SFT, Apache-2.0, of a strong small base on ~30,000 teacher traces, for what looks like a hundred optimizer steps on one GPU — and it visibly taught the model to state an answer and stop. That is worth having. Output discipline is most of what separates a base checkpoint from something you can put behind an API, and this run bought it for the price of a weekend. The GGUFs are correctly built, the header metadata is complete and truthful, the tensor tables reconcile to fourteen bytes of padding, and the SHA256SUMS file is there.

What the numbers do not support. "1 GB on a phone" — the smallest file is 1.312 GB, a third of it is one embedding table that llama.cpp will not quantize below Q6_K, and the weights are the cheap part once you ask for context. "GSM8K 33 → 64" — the same card prints 54.5 → 64.0 under a fixed extraction rule, and 69% of the advertised gain is the base's extraction penalty closing. "The full reasoning curriculum of a 2.4T flagship" — one epoch, 30k traces, 8,192-token window, loss on the prompts too. "262k with real reasoning" — the context is inherited and genuine, the reasoning was trained at 8k, and the two have never met.

What I could not settle. How much of MMLU's +0.265 is knowledge and how much is format. The base's strict-match of 0.004 means the strict column cannot see it, so the split that works cleanly on GSM8K does not transfer, and the ablation that would decide it — the base rescored few-shot or with a format-forcing prompt — was not run. I also could not verify the teacher traces, which come from "internal Qwen3.8 distillation datasets" that are not published, or check whether the eval used the chat template at all.

What I would watch. Publishers who report both lm-eval filters, and readers who look at the second one. Empero printed the strict-match column on all three parent cards and it undercuts their own headline at every scale — that is more disclosure than most releases manage. The failure is that the number 96% of their users see is the derivative card, which reprints only the flattering column. The fix is not more honesty from the publisher. It is that flexible-extract is a badly named filter that people read as "strict-match, but fairer", and it is neither.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Qwen3.8-2B-Distill: the filter wrote most of the headline", ai.thesatyajit.com, August 2026.

bibtex
@misc{ghana2026qwen382bdistill,
  author = {Satyajit Ghana},
  title  = {Qwen3.8-2B-Distill: the filter wrote most of the headline},
  url    = {https://ai.thesatyajit.com/articles/qwen3-8-2b-distill},
  year   = {2026}
}
share