Skip to content

Local inference field note / September 2026

The fastest local-LLM upgrade was remembering

Retesting Qwen3.8-27B on a 64GB M4 Max: the headline optimization did nothing, the most flattering metric pointed in the wrong direction, and an SSD cache cut a 91-second wait to 1.09 seconds.

1.09s
SSD-restored continuation, down from 90.93s cold
83×
less waiting by skipping repeated prefill
102
ordinary text-agent calls checked in the field
On this page
  1. 01Short answer
  2. 02What changed
  3. 03SSD cache
  4. 04Daily use
  5. 05Reasoning
  6. 06Block size
  7. 07ANE prefill
  8. 08Recovery
  9. 09System under test
  10. 10Hypotheses
  11. 11Baseline

The short answer

The largest speedup I found did not make the model generate faster. It stopped the model from repeating work it had already done.

Local-model tuning tends to orbit tokens per second: a new quant, a better drafter, a wider speculative block, another processor to offload work onto. Those numbers are easy to compare and satisfying to improve. But the delay I noticed most in daily use was elsewhere. I would return to an older Hermes conversation and wait a minute or more before the first new token appeared. The model was not decoding slowly. It was rebuilding a long prompt it had already processed.

Once oMLX's DFlash SSD prefix cache was working, a continuation of a 21,700-token conversation came back in 1.09 seconds instead of roughly 91 seconds. That was the only result in this round of tuning that immediately changed how the machine felt. It was also not the experiment I expected to win.

The system has three layers: Hermes manages the agent and its conversations, oMLX serves the model, and DFlash drafts tokens for Qwen3.8-27B to verify. “Prefill” is the work of reading the existing conversation before the model can generate the next token.

What changed

The practical answer is small: keep the model, drafter, context limit, and draft block that were already working. Enable SSD prefix restoration, trial low reasoning for the foreground agent, and leave ANE prefill off after its failures in long visual sessions.

Configuration after the experiments
SettingResult
Main modelQwen3.8-27B MLX 4-bit
DrafterQwen3.8-27B-DFlash2
DFlash maximum context98,304 tokens
Draft block size5 — unchanged
DFlash SSD prefix cacheEnabled, 20GiB limit
ANE prefillDisabled after repeated VLM/Metal failures
Hermes main agentLow reasoning effort, on trial
Hermes subagentsMedium reasoning effort
Vision recoveryManual reload; watchdog under field testing

Three findings support those choices. First, preserving completed prompt work removed far more waiting than accelerating it. Second, lower reasoning reached useful answers sooner by producing fewer hidden tokens. Third, the most attractive proxy metrics—ANE benchmark gains and draft acceptance—did not improve this system in ordinary use.

Why these choices survived

The fastest prompt is the one you do not process again

The in-memory DFlash prefix cache held four entries. That was enough for active conversations but not for the way I use Hermes across multiple sessions. Once an older session was evicted, returning to it meant reconstructing the entire prompt—about a minute and a half at 20K tokens, and potentially several minutes at larger contexts. oMLX 0.6.4 had added SSD-backed DFlash snapshots, but my configuration left them disabled.

My first test appeared to miss the cache because I resent the original prompt. A snapshot contains the prompt and its generated answer; lookup is designed for a continuation extending that stored prefix. Testing a real continuation changed the result.

Prefix restore paths
PathTime to first token
Cold processing, roughly 21.7K-token prompt90.93s
Continuation restored from memory0.79s
Continuation restored from SSD after eviction1.09s
On a first turn, DFlash computes a reusable prefix state and saves it to SSD. On a later continuation, 21,697 matched tokens are restored and only 25 new tokens require prefill.
The snapshot stores computed model state for the completed prefix. A matching continuation restores that state and processes only its new tail.

Runtime counters confirmed the SSD hit: 21,697 of 21,722 prompt tokens were restored. The meaningful comparison is not 0.79 seconds in memory versus 1.09 seconds on SSD—both are excellent—but 1.09 seconds versus 90.93 seconds after the memory entry is gone. The cache stayed enabled because it removes repeated work instead of merely making that work faster.

What the benchmark looked like in daily use

The controlled cache test was deliberately simple. Hermes is not. A normal turn can involve web searches, terminal calls, images, growing tool output, and context compression. I therefore checked the next stretch of ordinary use rather than assuming the 1.09-second result would describe every continuation.

Swipe to compare columns →

Hermes field trace after the controlled tests
Field pathCallsMedian promptPrefix reusedMedian prefillMedian request
No reusable prefix1429.0K0%121.57s140.94s
Prefix reused8834.2K98.2%3.92s19.28s

These were not identical prompts, so the request-time ratio is descriptive rather than a controlled speedup. The cleanest adjacent pair makes the mechanism clearer. A 30,515-token call with no reusable prefix spent 132.16 seconds in prefill. Its immediate continuation restored 30,510 of 30,659 prompt tokens and spent 1.15 seconds there.

Generation itself did not explain the experience. Across the larger trace, median decode throughput was 27.85 tok/s without a prefix and 27.45 tok/s with one; median draft acceptance was 67.9% and 70.25%. The large change was the work skipped before generation.

The visual workflow showed the same shape on a different engine. In seven VLM sessions, the first completion took a median 130.01 seconds; 32 later completions took a median 32.12 seconds. That is descriptive, not a controlled 4× speedup: outputs, tool payloads, and warm state all varied. In one lunch-photo loop, the first 41.7K-token call took 136.37 seconds; two immediate follow-ups finished in 25.53 and 18.59 seconds. Real agent loops were messier than the benchmark, but they supported the same conclusion: preserving history mattered more than squeezing a few more tokens per second from generation.

Less reasoning, not less capability

Qwen3.8's highest reasoning setting can spend a startling amount of time before producing an answer. The research suggestedmedium and proposed that shorter reasoning might also be easier for DFlash2 to predict. The experiment rejected that mechanism while making the cost of xhigh hard to ignore. “First answer” means the moment hidden reasoning ended and the final response began. “Total” includes the whole completion, so differently sized final answers can make it move in the opposite direction.

Swipe to compare columns →

Reasoning-effort sweep
EffortTaskFirst answerTotalReasoning characters
xhighCodeNo usable answer before token cap609.8s94,453
xhighProse379.8s404.3s54,602
mediumCode32.5s175.3s6,935
mediumProse45.8s65.1s7,150
lowCode21.7s184.9s4,456
lowProse4.9s38.2s759

The original xhigh code measurement overlapped another request, so I discarded it and repeated the prompt on an idle server. The clean run took 609.8 seconds, emitted 94,453 characters of reasoning, and exhausted a 24,576-token budget without producing the requested module. Prose independently showed the same tendency, waiting almost 380 seconds before its response began.

Across the sweep, however, DFlash acceptance remained in roughly the same 70–77% range. Low reasoning did not unlock a hidden speculative-decoding speedup. It reached the final answer sooner by generating far fewer reasoning tokens. This was not a clean win on every total-runtime number: the low-effort code completion ran longer than medium after its answer began.

Speed was only half the decision. Claude generated shuffled low-versus-medium pairs for TypeScript, prose, and a health-coaching conversation similar to my daily Hermes use. I preferred low on the health task, called the prose a tie, and found the TypeScript implementations comparable. Both TypeScript modules passed a fourteen-case differential test.

That is not enough evidence to declare low universally equal to medium. It is enough to run the main agent at low on trial for the workload I actually care about. Subagents remain at medium because their thinking is usually off the foreground critical path and these comparisons say little about longer tool-using loops. Low helps by doing less work before answering, not by making DFlash predict better.

A better-looking metric made the system slower

DFlash2 proposes several future tokens, and the full model accepts the valid prefix. It is tempting to optimize the acceptance percentage directly: more accepted tokens must mean more speed.

My draft block size was 5. Community results suggested trying 8 and 16. Both larger settings raised acceptance on the code prompt from 77.1% to 83.6%. They still lost.

Swipe to compare columns →

DFlash draft block size
Draft blockCode generationProse generationAcceptance, code / prose
5 — kept53.5–53.8 tok/s35.2–38.4 tok/s77.1% / 65.0%
852.2–52.4 tok/s25.6–26.0 tok/s83.6% / 66.2%
1652.4–52.5 tok/s25.6–26.0 tok/s83.6% / 66.2%

Code slowed slightly. Prose slowed by roughly 28%. Blocks 8 and 16 produced essentially identical results, suggesting that an effective limit elsewhere made them behave the same.

Acceptance is a ratio. Throughput is the objective. Drafting farther ahead can increase the fraction accepted while also spending more time proposing and verifying tokens that never become useful output. Block size 5 stayed.

Draft blocks 8 and 16 raise code acceptance but reduce prose throughput compared with block 5.
Acceptance described the mechanism. Throughput decided the setting.

The 57% optimization that did nothing

The most exciting community result was ANE prefill. oMLX can move part of Qwen's prompt-processing work from the GPU to Apple's Neural Engine. A published M3 Max test reported only 4% at 4K context, 13% at 16K, and 57% at 32K. My cold-prefill waits were the worst remaining latency in the stack, and the setting was off.

ANE off: 48.02 seconds. ANE on: 48.07 seconds. The difference was measurement noise.

The explanation was structural rather than statistical. In oMLX 0.6.4, the Qwen ANE patch is wired into the standard vision-capable, or VLM, engine. The DFlash engine loads its target through a different path and never enables the ANE prefill implementation. The setting was accepted, stored, and displayed, but silently did no work on the engine I actually use. Nothing crashed and the flag looked correct; only an engagement log or counter exposed the miss.

Text requests travel through DFlash and bypass ANE prefill, while image requests switch to the vision-capable engine where ANE can engage.
The configuration value was the same. The code path was not.

To measure the ANE implementation itself, I temporarily disabled DFlash and ran the VLM engine with the feature off and on.

Swipe to compare columns →

ANE prefill on the VLM engine
Prompt size, off / onANE offANE onThroughput gain
10,965 / 10,92744.06s38.09s+15.7%
21,632 / 21,69290.77s77.94s+16.5%
64,661 / 64,591322.42s281.94s+14.4%

The Neural Engine helped, but by 14–16% on this M4 Max, not 57%. The original 32K result was measured on a thermally soaked M3 Max and attributed part of its gain to relieving GPU thermal pressure. It was a valid result for that machine and protocol, not a portable constant.

Giving up DFlash to obtain the smaller prefill improvement would be a bad trade. I initially kept ANE for image requests, which force the model onto the VLM path, but two long-context visual turns ended in persistent Metal command-buffer errors inside ANE prefill. A measured speedup was not worth keeping on an unstable path, so I disabled it.

Operational caveat: recovery after an image

One operational problem remained from the original setup. When the DFlash engine receives an image, oMLX switches the model to its ordinary vision-capable engine. The switch is expected; DFlash cannot serve that multimodal turn. The surprising behavior is that the model can remain on the slower VLM engine for later text-only requests until it is unloaded and reloaded.

One screenshot can therefore cut subsequent generation speed roughly in half without producing an error. Manual reload remains the only recovery I consider dependable.

A possible watchdog could probe an idle model and check whetherspeculation.totals.requests increased; the VLM fallback does not increment that counter. It is a promising detection signal, not a finished safeguard. I would want substantially more ordinary visual use, recovery observations, and false-positive testing before recommending an automatic reload.

How I tested the choices

The system under test

My daily local agent runs on a Mac Studio with an M4 Max and 64GB of unified memory. The model is the regular mlx-community/Qwen3.8-27B-4bit checkpoint, served by oMLX 0.6.4, with incoai/Qwen3.8-27B-DFlash2 proposing future tokens for the full model to verify. Hermes Agent sits in front of it and handles conversations, tools, reminders, delegation, compression, and the rest of the application layer.

The first round of work had already settled the broad architecture:

  • 4-bit weights left enough memory for useful context and the drafter.
  • DFlash2 raised clean generation from roughly 23–25 tok/s without speculation to as much as 44–54 tok/s, depending on the output.
  • Hermes used a 65,536-token working context and compressed before reaching DFlash's 98,304-token ceiling.
  • Thinking was disabled for auxiliary work such as title generation and context compression, where it added cost without helping the result.
  • The in-memory DFlash prefix cache retained four entries within an 8GiB limit.

In real Hermes sessions, generation usually landed around 25–29 tok/s. A controlled code-generation prompt could reach 53.8 tok/s with 77.1% of drafted tokens accepted. Prose was slower, around 35–38 tok/s with 65% acceptance. Cold prompt processing remained the expensive phase: tens of thousands of tokens could take minutes before generation began. That was the question for this round.

Turning community advice into hypotheses

I asked Claude Code to research what the community had learned about Qwen3.8-27B since my original setup. The report returned a plausible list of opportunities:

  1. Enable Apple Neural Engine prefill. One M3 Max report showed a 57% improvement at 32K context.
  2. Reduce the model's reasoning effort from its expensive xhigh default.
  3. Enable the DFlash SSD prefix cache added in recent oMLX releases.
  4. Try larger DFlash draft blocks.
  5. Explore recovery from oMLX's persistent switch to the slower vision-capable engine after an image request.

The largest claim came from different hardware, engine, and checkpoint, so I treated the report as a queue of experiments rather than a configuration guide. Each test changed one variable and measured the mechanism it should affect; the harness rejected any result whose log window contained an overlapping request.

Baseline first

Before changing anything, I repeated a code task, a prose task, and a cold-prefill ladder.

Swipe to compare columns →

Baseline generation and draft acceptance
WorkloadGenerationDraft acceptance
Code53.5–53.8 tok/s77.1%
Prose35.2–38.4 tok/s65.0%

The acceptance rates repeated to the decimal. Cold prompt processing declined from about 229 tok/s at 10.9K tokens to 204 tok/s at 43.2K, and five planted facts survived every tested length. The baseline was stable enough to challenge. It also showed why one tokens-per-second number cannot describe a model: structured code was much easier for the drafter to predict than open-ended prose.

Sources and reproducibility

The measurements, raw harness output, blind-comparison artifacts, and watchdog code were retained with the experiment. The public references below cover the model, drafter, runtime release, and the community reports that became testable hypotheses.

The first xhigh code run is intentionally absent from the table because a scheduled request overlapped it. The clean idle-server rerun is the reported 609.8-second result.

Measured on a 64GB M4 Max using Qwen3.8-27B MLX 4-bit, DFlash2, and oMLX 0.6.4. Controlled tests and the Hermes field trace are labeled separately so synthetic repeatability is not confused with daily-use variability.