Skip to main content

3 posts tagged with "coherence"

View All Tags

It's a Cliff, Not a Slope

· 8 min read
Rajveer Rathod
Author of VeloxQuant-MLX

Third post in the QFilters sub-series, following the original collapse and the calibration follow-up. Both earlier posts established that qfilters_budget=512 breaks generation on a 2,238-token prompt, and that raising the budget above the prompt's length fixes it. This post asks the question those two left open: what does the space between those two points actually look like?


The calibration follow-up ended with real calibration making the collapse faster and differently-shaped, but not fixed, at the method's default qfilters_budget=512. A natural next question -- natural enough that it's the direct answer to "how do I get coherent output" -- is whether raising the budget helps, and if so, how much is enough. A quick check confirmed the extreme case: a budget of 3,072 against this same 2,238-token prompt means nothing gets evicted at all, and the output is fluent, nearly matching fp16.

That's a single before/after comparison, though -- "broken at 512, fine at 3072" -- and it doesn't say whether quality degrades gradually as the budget shrinks toward the prompt length, or whether something sharper is going on. This post sweeps nine budget values between those two points to find out.

Setup

Same long prompt (2,238 tokens), same calibrated filter path from the previous post (established as faster and no worse than the fallback at equal budget), same model. Nine qfilters_budget values -- 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560 -- each run for 3 interleaved repeats at 120 max tokens, plus an interleaved fp16 reference. Quality tracked via word overlap against the fp16 reference text (the same metric the earlier posts used), plus throughput, peak memory, and the realized compression ratio at each budget.

Why interleave fp16 with the sweep instead of reusing one reference

The earlier posts computed fp16 once and compared everything against it. This script regenerates fp16 with its own repeats, interleaved into the same run as the swept budgets, so the reference shares the identical noise floor as everything it's compared against -- consistent with this series' standing discipline against blocked, non-interleaved comparisons.

The numbers

budget% of prompttok/s medianpeak MBcompressionword overlap (fp16)
51222.9%3.0859074.60x0.000
76834.3%3.3958673.07x0.000
102445.8%3.5160352.30x0.014
128057.2%3.4162031.84x0.014
153668.6%3.2263711.53x0.014
179280.1%2.8165401.32x0.014
204891.5%2.8161391.15x0.300
2304102.9%3.1161391.02x0.811
2560114.4%3.0861391.00x0.811
fp16 (reference)--6.065226--1.000
This is a cliff, not a slope

From budget=512 through budget=1792 -- retaining anywhere from 23% to 80% of the prompt -- word overlap against fp16 sits flat at 0.000-0.014. Every one of those six configurations produces the same category of unusable output. Then between 1792 and 2048 (80.1% to 91.5% of the prompt), overlap jumps more than 20x, to 0.300, and by 2304 (just over the full prompt length) it reaches 0.811 -- most of the remaining gap from there to fp16's 1.000 is the ordinary model-level variation this series has seen throughout (fp16 itself doesn't reproduce its own text as its only possible output under different cache states; word overlap near 0.8-0.9 between two fluent generations of the same prompt is a normal ceiling, not a defect). Retaining even 80% of a prompt's tokens was not "mostly enough" here -- it was functionally the same as retaining 23%.

What the text actually looks like on either side of the cliff

budget=1792 (80.1% of prompt retained, all 3 repeats identical):
the model. the. the. the. the. the. the. the. the. the. the. the. the. the. the...

budget=2048 (91.5% of prompt retained, all 3 repeats identical):
Also, explain why the model weights are not the main constraint.

Answer in English, in a clear and concise manner, using simple terms. Do not use
markdown.

Okay, so I need to explain why the KV ca...

fp16 (reference):
Also, explain in simple terms what the KV cache is, and what it does.

Finally, explain in simple terms what "long-context inference" means.

Please make sure your explanations are clear and conci...

budget=1792 is the same shape of failure as the original post's budget=512 run -- deterministic, fully repeated, not language. budget=2048 is genuinely coherent: on-topic, grammatical, responding sensibly to the prompt's actual instructions, even though it's not byte-identical to fp16. The transition between those two adjacent sweep points is the entire story of this post.

Compression ratio was never the useful signal here

Look at the compression column: it declines smoothly and predictably as budget rises (4.60x -> 3.07x -> 2.30x -> ... -> 1.00x), exactly as the arithmetic of "keep more tokens, compress less" would predict. There is nothing in that column that would tell you where the cliff is. A dashboard reporting "compression ratio: 1.32x, throughput: 2.81 tok/s" at budget=1792 looks unremarkable sitting next to budget=2048's "compression ratio: 1.15x, throughput: 2.81 tok/s" -- identical throughput, similar compression, and one of them is nonsense text. Compression ratio and throughput both move smoothly through the cliff; only checking the actual output catches it.

Peak memory doesn't move the way the eviction story would suggest, either

Peak memory rises from 512 through 1792 (5907MB -> 6540MB) despite compression falling over that same range -- more retained tokens should mean less relative savings, but here it also costs more absolute memory, consistent with the earlier calibration post's finding that scoring/bookkeeping overhead is a real, separate cost from the compression itself. It then drops and flattens at 6139MB for 2048 through 2560, once eviction stops actually triggering on this prompt. None of this tracks the coherence cliff either -- it's a separate axis, moving on its own schedule.

What this means for actually using QFilters

The original post's finding stands: the method's own default (512) is unusable on a prompt anywhere near this length. The calibration follow-up's finding also stands: calibration alone doesn't rescue it at that budget. What this post adds is the concrete, actionable number this whole line of investigation was aimed at:

The usable range on this prompt starts around 90-95% of the token count

For this specific model, prompt, and calibration, quality was unusable at 80% retention and fine at 92% retention. That is a narrow band to land a production budget in -- there is very little room between "compresses meaningfully" and "keeps enough to be coherent" for this prompt shape. A budget chosen as a round number without checking against the actual prompt distribution it will see in production (this run's 512 default being a clear example) is not a safe default; it's closer to a coin flip that happened to land on "broken" here.

A cliff is worse for production than a slope, not better

A gradual quality decline is something a budget-vs-quality tradeoff curve can reason about -- pick a point, accept the cost. A cliff means most of the budget range you might reasonably try gives you no information about how close you are to the failure boundary until you cross it. Throughput and compression ratio looked smooth and well-behaved at every point in this sweep; nothing in those numbers would have warned that 1792 and 2048 were on opposite sides of a discontinuity.

This threshold is not a universal QFilters number

80-92% of this specific 2,238-token prompt, on this specific calibration, on Qwen3-8B. A different prompt (different redundancy, different structure, different dependency on far-back tokens) would plausibly put the cliff somewhere else entirely -- possibly at a much lower retention fraction for a more repetitive prompt, or requiring closer to 100% for one that depends heavily on early-context details. The number this post found is a real, measured data point for this exact setup, not a formula.

Four posts into the QFilters line of this series, the shape of the finding has moved from "it's broken" to "it's broken here, fixed there" to, now, "here's exactly where the line is, and it's much closer to 'don't evict much at all' than the method's own 512-token default would suggest."


Benchmarked on an Apple M4 (10-core GPU, 24GB unified memory) against mlx-community/Qwen3-8B-4bit (36 layers, 8 KV heads, head_dim 128), calibrated Q-Filters (see the calibration follow-up), 3 interleaved repeats per budget, 120 max tokens, long prompt only (2,238 tokens). Sweep script: benchmark_scripts/benchmark_qwen3_8b_qfilters_budget_sweep.py. See the original QFilters post and the calibration follow-up for the findings this one builds on.

Calibration Changed the Failure, Not the Outcome

· 8 min read
Rajveer Rathod
Author of VeloxQuant-MLX

Direct follow-up to The Output That Stopped Being Output. That post found QFilters producing a fully reproducible generation collapse on a long prompt, using the fallback (uncalibrated) filter path. This one asks the obvious next question: does this repo's real calibration module fix it? Short answer -- it changes what the collapse looks like and makes it substantially faster, but on this exact configuration, it does not produce coherent output.


The QFilters post was explicit about running the uncalibrated fallback path -- key-SVD-estimated filter direction, sign ambiguous, because a KV cache never sees queries. That was flagged as a real limitation, not a hidden one. What made it worth a follow-up is that this repo isn't missing a fix for that limitation: veloxquant_mlx/quantizers/qfilters_calibration.py already implements the paper's actual mechanism (arXiv:2503.02812 §3.2) -- hook each layer's query projection, gather real query activations, take the SVD's top right-singular vector per head, sign-fix it against the paper's Theorem 3.3 (kappa^h > 0), average query-head filters down to KV heads for GQA. It just wasn't used in the first post.

This post uses it.

What calibration actually involved

calibrate_qwen3_8b_qfilters.py ran Qwen3-8B forward over five ~2,048-token calibration passages (general prose about transformers, memory bandwidth, unified memory, vector quantization, and eviction itself -- topically adjacent to the benchmark prompt but not the same text), captured real query activations per layer via a hook on q_proj, and computed one [8, 128] filter per layer (32 query heads averaged down to 8 KV heads). This is a one-time, pre-deployment pass -- the paper's own cost estimate is 20 samples of length 2048; this run used 5, on the smaller side but the same order of magnitude.

This is not a claim about calibration-corpus quality

The calibration passages here are repo-local synthetic prose, not the Pile (what the paper's own experiments used). A better-matched, larger, more diverse calibration corpus is a real variable this post did not sweep. What this post answers is narrower: given the calibration mechanism this repo already implements, run correctly, does it fix the specific collapse the earlier post measured?

The comparison

Same long prompt (2,238 tokens) and same qfilters_budget=512 as the original post, five arms instead of three: fp16, fallback_off/fallback_on (the original post's uncalibrated path, Metal kernel off/on), and calibrated_off/calibrated_on (this post's real query-SVD filters, Metal kernel off/on). 5 interleaved repeats per arm.

armtok/s medianmin-maxpeak MBvs fp16compression
fp166.475.40-7.055226100%1.00x
fallback (off)0.950.86-1.12526714.7%4.60x
fallback (on)0.910.83-1.12526614.1%4.60x
calibrated (off)3.413.14-4.23556552.7%4.60x
calibrated (on)3.320.26-4.09554751.3%4.60x
Real calibration is 3.6x faster than the fallback, at the same compression

calibrated_off runs at 3.41 tok/s median vs. fallback_off's 0.95 -- a 3.6x speedup, at an identical 4.60x compression ratio (the budget and eviction count are the same; only the scoring direction changed). This wasn't the thing being tested for, but it's a real, reproducible difference: the calibrated filter evicts along a more decisive direction, which appears to change the per-step cost of the eviction path itself, not just its selection quality. Peak memory is about 6% higher with calibration (5565MB vs. 5267MB) -- a real, if modest, cost for that speedup.

One outlier worth naming instead of hiding

calibrated_on's five repeats ranged 0.26 to 4.09 tok/s -- one rep came in far below the other four (which clustered 3.3-4.1). Every other arm in this run, and every arm across all four posts in this series, has shown tight repeat-to-repeat clustering. This looks like a single stray contention or thermal event rather than a structural property of calibrated eviction, and the median (which this table reports) is barely moved by it -- but a benchmark that only ran once could have reported either 0.26 or 4.09 as "the" number, and both would have been wrong. This is the same noise-floor discipline the first post in this series built the whole protocol around, applied here.

The actual output

Every arm was internally deterministic -- 5/5 identical repeats, same as every method tested in this series. Here's what changed and what didn't:

fallback (both kernel arms, all 5 repeats):
===============.========================================================================= is is is is is is is is is is is is is is is is is is is is===========

calibrated (both kernel arms, all 5 repeats):
...........,. the..........,. the. the. and.,..... the.,. the. the. and. the. the. the. and. the. the. the. the. the. the. the. and. the. the. the. the...

Calibration produced a different deterministic collapse, not a working one. fp16-vs-calibrated word overlap is actually lower (0.000) than fp16-vs-fallback (0.014) -- by that specific metric, calibration didn't move the output closer to the uncompressed baseline at all.

Not a fix, but not nothing either

There's a real, if easy to over-read, qualitative shift: the fallback path collapses into = and a single repeated verb; the calibrated path collapses into real English function words (the, and) and sentence-ending punctuation, with more variety in what repeats. That's a different kind of broken -- closer to the shape of degenerate language-model output (function-word loops are a known failure mode even in undamaged models under some decoding settings) than to pure symbol repetition. But "closer to the shape of a failure mode we recognize" is not "coherent," and nothing here should be read as "calibration solved it." At qfilters_budget=512 against a 2,238-token prompt whose question requires reasoning over content spread across that whole passage, this specific configuration still does not produce usable output, calibrated or not.

What this changes about the original post's finding

The original QFilters post's headline claim holds: past this method's default budget, on this prompt, output collapses -- deterministically, reproducibly. What this follow-up adds is that the collapse is sensitive to filter quality (it's not simply what eviction always looks like at this budget, since a better filter changed its shape and roughly quadrupled the achievable compression-adjusted throughput) but is not resolved by filter quality alone at this budget. The next honest variable to sweep isn't calibration -- it's the budget itself, or a larger/better-matched calibration corpus, or both. Neither was tested here.

What I'd take from this

Test the fix, don't assume it from the docstring

"A real calibration module exists in this repo" was true before this post ran and would have been an easy thing to cite as sufficient. It wasn't -- the module does what it says (real query-SVD, sign-fixed, GQA-averaged), and using it correctly still didn't produce coherent output at this budget. The only way to know that was to run it.

A speedup and a coherence fix are independent axes, again

The VecInfer post found a real kernel speedup with a correctness gap. This post finds a real calibration-driven speedup (3.6x) with no coherence fix. Two different mechanisms, same lesson: throughput moving in the right direction says nothing about whether the text is any good, and the only way to know is to read the text.

A negative result on the fix is still a result

The header of this post could have been "calibration solves it" (flattering, and false) or nothing at all (if the run had been quietly discarded for not showing the hoped-for improvement). Reporting "it's different and faster but still not coherent" is the accurate middle ground, and it's the more useful one for anyone deciding whether to trust QFilters at this budget in production.

So what budget do you actually need?

This post held qfilters_budget=512 fixed and varied the filter quality. The next post in this sub-series does the opposite -- It's a Cliff, Not a Slope holds calibration fixed and sweeps the budget from 512 up to and past the prompt length, and finds the transition from broken to coherent is a sharp threshold around 80-92% of the prompt's token count, not a gradual slope.


Benchmarked on an Apple M4 (10-core GPU, 24GB unified memory) against mlx-community/Qwen3-8B-4bit (36 layers, 8 KV heads, head_dim 128), QFilters qfilters_budget=512, 5 interleaved repeats, 120 max tokens, long prompt only (2,238 tokens -- the configuration that exceeds budget). Calibration script: benchmark_scripts/calibrate_qwen3_8b_qfilters.py. Comparison script: benchmark_scripts/benchmark_qwen3_8b_qfilters_calibrated.py. See the original QFilters post for the finding this follows up on, and qfilters_calibration.py for the calibration mechanism itself.

The Output That Stopped Being Output

· 10 min read
Rajveer Rathod
Author of VeloxQuant-MLX

Third post in this series, same model and protocol as the KIVI and TurboQuantRVQ posts before it. This time the method doesn't quantize anything -- it evicts. And once the cache runs out of budget, the honest result isn't a percentage, it's forty repeated equals signs.


The first post in this series found a Metal kernel with zero end-to-end effect. The second found a real throughput cost and a memory claim that flipped sign at longer context. Both methods, though, were quantization -- every key and value survives, just approximated. Nothing in either post could produce genuinely broken output, because nothing was ever thrown away.

Companion post

A fourth post in this series runs the same protocol against VecInfer, another quantization method -- The Kernel That Finally Did Something finds the first Metal kernel in the series that is both a real, large speedup (up to 14x) and, unlike every kernel tested so far including this post's, not byte-identical to its own pure-MLX fallback.

This post benchmarks QFilters, this repo's query-agnostic eviction method, which works differently on purpose: past a fixed token budget, it drops the lowest-scoring cached tokens outright. Their information doesn't get approximated -- it's gone. That makes a different question testable, one the first two posts structurally couldn't ask: does the cache stay coherent once it starts forgetting things?

Why eviction is a different kind of test

QFiltersKVCache scores every cached key by its projection onto a frozen per-head direction -- the "Q-Filter" -- and evicts the lowest-scoring tokens once the cache exceeds qfilters_budget (default 512 tokens, including protected leading "sink" positions). This repo's implementation is explicit that it's adapted, not a faithful port of the arXiv:2503.02812 preprint, and offers two ways to get the scoring direction:

  • Calibrated filters (query-SVD, the mechanism the paper actually specifies) -- frozen before the first token, correct by construction, path-independent.
  • Fallback (filters=None) -- SVD-estimated from the first qfilters_calib_tokens observed keys, which recovers the dominant axis but not its orientation. The module docstring calls this exactly what it is: a real, documented mode, not a stand-in.

No calibrated filters exist for Qwen3-8B in this repo, so this run used the fallback path -- the honest choice given what's actually available, not the paper's strongest configuration. That's flagged here the same way the TurboQuantRVQ post flagged using the method's own uncalibrated default bit-width: report what you actually ran, not the best-case version of it.

VecInfer -- this repo's other codebook-based method with a public Metal toggle -- was considered and set aside for this run. Its docstring is explicit that a random-initialized codebook is "for tests only," and no calibration tooling exists in this repo yet to produce a real one. Benchmarking coherence against a method whose codebook is documented as non-functional would have answered a different, less honest question than the one this post is actually asking.

Setup

Same protocol as the previous two posts: fp16 vs. kernel-off vs. kernel-on, interleaved (not blocked) repeats, median/min/max reported, two prompt lengths, a GPU-contention check before trusting anything. One addition specific to eviction: two separate comparisons that must not be conflated.

Two different questions, two different expectations
  1. qfilters_on vs. qfilters_off (fused Metal eviction kernel vs. the pure-MLX path) -- the module docstring claims these agree bit-for-bit, same tie-breaking convention. This should produce identical text, exactly like the KIVI and TurboQuantRVQ kernel checks.
  2. QFilters vs. fp16 -- this should not be expected to match once the cache exceeds budget. Tokens are being discarded, not approximated. Divergence here isn't a bug to rule out; it's the actual thing this post measures.

The short prompt (231 tokens) stays under the 512-token budget -- no eviction happens at all. The long prompt (2,238 tokens) blows well past it. That contrast is the point: one prompt length isolates pure per-step scoring overhead, the other tests what happens once the method actually does its job.

The numbers

Run against mlx-community/Qwen3-8B-4bit on Apple M4 (24 GB), 36 layers, 8 KV heads, head_dim 128, qfilters_budget=512, fallback (uncalibrated) filters, 5 interleaved repeats:

promptarmtok/s medianmin-maxpeak MBvs fp16 tok/scompression
short (231 tok, no eviction)fp1616.9116.77-18.114725100%1.00x
shortQFilters off11.8511.59-12.35493370.1%1.00x
shortQFilters on11.7811.29-12.36493369.7%1.00x
long (2,238 tok, heavy eviction)fp165.585.48-5.855226100%1.00x
longQFilters off0.890.88-0.91526716.0%4.60x
longQFilters on0.890.83-0.90526615.9%4.60x
Metal eviction kernel, on vs. off: bit-exact again

Byte-identical output across all ten repeats at both prompt lengths -- the third method in this series where the fused Metal kernel and the pure-MLX fallback produce indistinguishable text. The pattern established by KIVI and reconfirmed by TurboQuantRVQ holds a third time: this repo's Metal kernels are correct by the bit-exactness standard they claim, consistently, across three structurally different methods.

Finding 1: overhead exists even with zero evictions

At the short prompt, nothing gets evicted -- the cache never reaches its 512-token budget. And yet QFilters is 30% slower than fp16, with peak memory 4.4% higher, not lower. This is pure per-step scoring cost: projecting every cached key onto the filter direction, tracking per-head state, maintaining the SVD estimation buffers for the fallback path -- all of it runs whether or not anything ends up evicted. Unlike KIVI (roughly free) or TurboQuantRVQ (a real but smaller 15% cost at this same prompt length), QFilters' overhead is front-loaded into bookkeeping that has nothing to do with the compression it eventually delivers.

Finding 2: at long context, throughput drops 84% and compression still doesn't shrink peak memory

Once eviction is actually happening, throughput falls to 16% of fp16's -- a bigger relative cost than either quantization method in this series produced at any context length. Compression is real here (4.60x on the retained K/V, since evicted tokens' bytes are actually gone, not just packed tighter), but peak memory is still 0.78% higher than fp16, not lower. The same shape of result as the TurboQuantRVQ post's memory-claim reversal, but more extreme: even a genuine 4.6x compression ratio on retained tokens doesn't translate into a net memory win, because whatever eviction bookkeeping costs at 2,238 tokens of context outweighs it.

Finding 3: the coherence question has a real, fully reproducible answer

This is the part that doesn't fit in a percentage. Here is what fp16 generated on the long prompt (first 200 characters, identical across all 5 repeats):

fp16:

Also, explain in simple terms what the KV cache is, and what it does.

Finally, explain in simple terms what "long-context inference" means.

Please make sure your explanations are clear and conci...

Here is what QFilters generated on the same prompt -- identical across all 5 repeats, and identical between the Metal kernel on and off:

qfilters (on and off, all 5 repeats):
===============.========================================================================= is is is is is is is is is is is is is is is is is is is is===========
Not noise -- a deterministic failure mode

This isn't a one-off bad sample. All five repeats of qfilters_off and all five of qfilters_on produced the exact same degenerate string, character for character. That rules out the ordinary explanations -- sampling variance, a rare unlucky seed, thermal-state luck. Once the fallback filter's SVD-estimated direction evicts enough of the wrong tokens, the model's context is damaged badly enough that generation collapses into token repetition (is is is is...) and separator spam (====), and it does so exactly the same way every time. That's what a genuinely broken KV cache looks like at the output layer, not a subtle quality regression you'd need an eval harness to catch.

Worth being precise about what this does and doesn't indict. This is the uncalibrated fallback path -- the docstring is explicit that a key-SVD-estimated direction "recovers the dominant axis but not its orientation," i.e., it can evict in the wrong direction from the start. It is not a claim that QFilters with real calibrated query-SVD filters would fail the same way; that's a different, untested configuration. It's also not a claim about the eviction mechanism being unsound -- a budget of 512 tokens against a prompt that needs most of its 2,238 tokens of context to answer a question about that passage is close to an adversarial setup by construction. What it is: an honest measurement of what actually ships if this cache is deployed at its own default budget, with the calibration path that's actually available in this repo today, against a plausible long-context prompt.

The fp16 baseline here isn't polished either -- it echoes the prompt's own instructions rather than producing a tight two-paragraph explanation, which is a model/prompt-fit issue independent of any cache method. But "echoes the prompt" and "===== is is is is is =====" are not the same category of failure, and the gap between them is the actual finding.


What I'd take from this

Quantization and eviction are different bets

KIVI and TurboQuantRVQ can be wrong about a percentage -- fp16 similarity, throughput, memory. QFilters, past its budget, can be wrong about whether the output is language at all. Reporting "compression ratio: 4.6x" without also reporting what the output looked like at that ratio would have been true and almost useless.

Overhead and compression are separable, and the short prompt proves it

A method can cost real throughput and memory even in the exact case where it does nothing -- zero evictions, zero compression, and still 30% slower with higher peak memory than fp16. If a benchmark only runs prompts long enough to trigger the mechanism, this cost is invisible. The short prompt in this run existed specifically to catch it.

Determinism across repeats is itself informative

Five bit-identical repeats of a degenerate output is stronger evidence than one bad sample and stronger evidence than five different bad samples. It says the failure is structural -- reachable from this exact configuration every time -- not a rare unlucky draw this benchmark happened to catch once.

Three methods into this series, three different shapes of honest result: a kernel that changes nothing, a method with a real and growing cost and a memory claim that reverses sign, and a method where the Metal kernel is once again bit-exact but the method itself, run at its own default budget with the calibration path actually available, can turn a long-context prompt into forty characters of =.

Does real calibration fix this?

This post used the uncalibrated fallback filter path. This repo also has a real calibration module implementing the paper's actual query-SVD mechanism -- Calibration Changed the Failure, Not the Outcome runs it and finds a 3.6x throughput improvement and a different collapse pattern, but not coherent output, at this same budget.


Benchmarked on an Apple M4 (10-core GPU, 24GB unified memory) against mlx-community/Qwen3-8B-4bit (36 layers, 8 KV heads, head_dim 128), QFilters qfilters_budget=512 (the method's own default), fallback (uncalibrated) filter path, 5 interleaved repeats per arm, 120 max tokens. Benchmark script: benchmark_scripts/benchmark_qwen3_8b_qfilters_honest.py. See the KIVI/Qwen3-8B post and TurboQuantRVQ/Qwen3-8B post for the first two posts in this series, and qfilters_cache.py for the method's implementation, its calibrated-vs-fallback filter distinction, and its own documented limitations.