Optimizing local AI: kernels, prefill and decode

Getting a large model to load on my hardware was the first milestone, not the finish line. The harder work was making it numerically correct, useful under a real agent workload, and measurable without the benchmark fooling me.

Hardware
RTX PRO 6000 Blackwell & Apple Silicon
Recorded work
April–September 2026
Success criterion
Correct, completed work—not peak tokens/s

What I found

  1. After a system update, direct cross-GPU copies mismatched in all 20 recorded trials while host-staged copies matched in all 20, though the driver reported peer access as available.

  2. In a five-way concurrent run, queued requests waited about 45 and 57 seconds for a first token, while generation ran at about 61–66 tokens per second.

  3. MTP depth 4 lifted decode from 113.9 to 242.5 tokens/s and C=16 aggregate from 996 to 1,739; depth 12 reached 247.2 decode but only 1,250 aggregate.

  4. Five-task coding checks scored 400/400 on the functional portion for both MTP runs and the no-MTP control; totals were 475, 480 and 482 out of 500.

The central idea

Optimize the complete path from prompt to verified artifact. Faster decoding, accurate evaluation, and stable long-running agents are separate engineering problems that have to work together.

  1. 01Prove compatibility
  2. 02Test correctness
  3. 03Measure the workload
  4. 04Tune one variable
  5. 05Recheck task quality
My bring-up ladder · speed changes earn their place only after correctness and task checks
Contents
  1. What I found
  2. The engineering starts before the benchmark
  3. Prove that the fast path is still correct
  4. Prefill, decode and waiting are different measurements
  5. A concrete tradeoff: speculative depth versus useful throughput
  6. The chosen configuration needed a quality check, too
  7. What actually serves on the workstation
  8. Better evaluators required software work, not just more GPU
  9. How much of this work did I actually record?
  10. Why I keep doing this locally
  11. Sources & scope

The engineering starts before the benchmark

My local stack has involved serving engines, quantized checkpoints, GPU kernels, tool parsers, caches and evaluation adapters. Much of the underlying kernel work comes from open-source maintainers and community builds. My contribution has been hands-on integration, local patches, controlled bisects, configuration tuning, and finding where apparently compatible pieces stop agreeing.

The DeepSeek bring-up records show why a supported GPU architecture is not a yes-or-no property of a whole model. An attention kernel could work on workstation Blackwell while an output projection or expert kernel still assumed a different architecture. A capability check could pass while a compiled wheel lacked the necessary implementation. Changing a version label was not enough.

The successful path required matching the compiler and runtime, selecting compatible kernels, managing loading-time memory separately from steady-state memory, and testing the resulting system. I would not attribute the later speed gains entirely to my patches: checkpoint, quantization, engine and speculative-decoding changes also mattered.

Prove that the fast path is still correct

An August Qwen3.8-27B campaign separated the model, quantization and serving path rather than judging the model from one bad output. One bisect isolated a failure to an FP8-plus-MTP combination in that particular engine. FP8 without multi-token prediction behaved correctly in both eager and compiled modes; another quantized path with MTP also passed the recorded checks. The interaction mattered more than either feature’s name.

A separate multi-GPU incident involved the Linux kernel, not a CUDA compute kernel. After a system update, the driver reported peer access as available, but real transfers told a different story. Direct cross-GPU copies mismatched in all 20 recorded trials; host-staged copies matched in all 20. Collective communication could hang as well.

The workaround avoided the broken peer-to-peer path and tested actual data movement. That experience made a small numerical correctness probe part of the bring-up process. A fast server that silently changes data is not an optimization, and an advertised capability is not an end-to-end test.

Prefill, decode and waiting are different measurements

Prefill processes the prompt; decode generates new tokens. Time to first token also includes waiting for a server slot and other overhead. An agent with a long history can spend much of its time on prompt processing even when its visible token stream looks fast.

My April concurrent benchmark recorded first-token timing, generation timing and task results separately. In a five-way run, front-of-queue requests reached a first token in roughly half a second, while queued requests waited about 45 and 57 seconds. Dividing their prompt lengths by that wait produced a misleadingly terrible “prefill” rate. It measured queueing as well as computation.

The post-first-token generation rates in that run were much closer—about 61–66 tokens per second. The lesson was not that the kernel suddenly became a hundred times slower for one request. It was that I needed to distinguish queue delay, prompt processing, per-user decode speed and aggregate throughput.

For long-running agents, I also watch the growth of the conversation and the useful cache working set. A fast short completion is not representative of repeated tool calls carrying a large history. Cached and uncached prompt tests need to be labeled, not blended into a single speed claim.

A concrete tradeoff: speculative depth versus useful throughput

An August 27 Qwen3.8-Flash-Next sweep on my two-GPU workstation tested multi-token prediction (MTP), which proposes several tokens ahead for verification. Deeper speculation improved the isolated decode number slightly, but did not improve every workload. It also consumed memory that otherwise supported the key/value cache.

The table is a dated configuration sweep, not a current hardware leaderboard. C=16 means aggregate throughput with sixteen concurrent requests; it is not the speed an individual agent saw. The launch choice also kept memory headroom for the workstation rather than using the most aggressive setting.

Recorded Qwen3.8-Flash-Next sweep · August 27, 2026 · same two-GPU workstation; configuration varies
ConfigurationDecode tokens/sPrefill tokens/sC=16 aggregate tokens/s
No MTP baseline113.910,043996
MTP depth 4242.57,5961,739
MTP depth 12247.27,4761,250
Selected: depth 4, batch 16,384, memory utilization 0.92233.89,2431,712

The chosen configuration needed a quality check, too

Increasing the batch-token limit recovered much of the prefill throughput lost to speculation, while giving up some isolated decode speed. Depth 12 offered little extra decode speed and substantially worse concurrent throughput than depth 4. The choice was a workload tradeoff, not a hunt for the largest number in one column.

The accompanying five-task coding checks recorded 400/400 on the functional portion for both MTP runs and the no-MTP control. Total scores were 475, 480 and 482 out of 500; the spread came from the separate code-quality heuristic. That small sample did not demonstrate a general quality loss—or prove that MTP is always quality-neutral.

These are not the four-task Prime agent scores discussed in my other evaluation note. They come from a different suite and should not be combined. The campaign also had several generations of answer extraction and sampling adapters. A number without its adapter version can be an invalid comparison.

What actually serves on the workstation

The launch recipes behind this note are public as blackwell-serving-kit, with the DeepSeek work in its own repository. The table is the measured state of each configuration on the two-GPU workstation between April and September 2026: one launcher per row, the single-stream decode rate, and the aggregate rate at the concurrency noted. Rows are not interchangeable: different models, quantizations, engines and dates.

Two of the findings in the repository are worth stating here. The peer-to-peer workaround described above cost nothing measurable on the DeepSeek serve (185.2 tokens per second with P2P disabled against 185.7 with it on), and a two-minute probe now runs after every kernel update before any engine flag is blamed. And the GLM-5.3-Flash NVFP4 checkpoint did not fit for two independent reasons that are written up separately: a quantization that left the attention stack in BF16, and an SM120 sparse-attention kernel that asserts on models without rotary position dimensions.

Measured serving configurations · two RTX PRO 6000 (SM120) unless noted · April–September 2026 · from the blackwell-serving-kit README
ModelFormat and engineSingle streamAggregate
Qwen3.8-Flash-Next (125B, 6B active, 51B n-gram table)FP8, vLLM TP2, table offloaded to host RAM, MTP depth 4233.8 tokens/s (113.9 without MTP)1,712 tokens/s at 16 streams
Qwen3.8-Flash-Next, one GPUNVFP4, same image with a one-file loader patch90.6 tokens/s496 tokens/s at 16
DeepSeek V4-FlashNVFP4, community B12X vLLM build with the MTP fix108.8 to 150.6 tokens/s (+38% from MTP)—
DeepSeek V4-FlashFP8, DSpark speculative build182 to 186 tokens/s201 tokens/s at 4
Ornith-1.5-35B-A3Bbf16, stock vLLM, one GPU179.5 tokens/s3,444 tokens/s at 128, not saturated
Ornith-1.5-397BNVFP4, vLLM TP2, expert tensors offloaded to pinned RAM10.9 tokens/s12.5 tokens/s at 4, PCIe-bound
GLM-5.3-FlashTR3 4-bit experts, the author’s vLLM fork108 to 117 tokens/s105 to 185 tokens/s at 4
MiniMax M2.7NVFP4, SGLang with b12x kernels120 to 127 tokens/s408 tokens/s at 16
Qwen3.8-27B on a Mac Studio M3 UltraoQ4e, oMLX dual-ANE prefill and MTP81 tokens/s on code, 59 on prose78 tokens/s at 4; prefill +22% at 32K

Better evaluators required software work, not just more GPU

Kernel tuning and evaluator quality are connected operationally, but they are not the same improvement. A faster inference path does not make a score more meaningful. In one coding incident, fixing final-answer extraction changed the recorded score from 17/100 to 97/100 without changing the model’s answer at all.

For video, using a persistent, stronger speech-recognition service avoided repeatedly loading a small local transcriber and helped keep rendering and evaluation from fighting over resources. The current LTX quality-gate code can still fall back to a smaller transcriber. That fallback must travel with the result: changing the evaluator backend can change what a word-error score means.

I also check evaluator availability and parsing explicitly. An empty extracted script or a skipped visual check is missing evidence, not a passed creative requirement. These repairs improve the validity of the evaluation; serving and kernel work make it more practical to run it often.

On the agent side, tool parsers, sampling defaults and reasoning budgets affect whether inference becomes action. A local GLM experiment became much more useful with a bounded thinking budget rather than letting reasoning consume the turn. Timeouts and context-limit exits remain inconclusive capability results, while still counting as unfinished work for the person waiting.

How much of this work did I actually record?

There is a substantial trail: dated experiment notes, launch recipes, benchmark runners, raw run artifacts, a factorized Qwen campaign manifest, and correction notes when an earlier explanation turned out to be wrong. Together they document months of bring-up and optimization work.

They are not a complete timesheet. Some old checkpoints were removed for storage, some settings changed between campaigns, and some investigations ended with a working recipe rather than a final report. I can reconstruct the sequence below; I cannot honestly turn it into a precise lifetime total of hours or an across-the-board speedup.

A reconstruction from dated records—not a complete work log
Period in 2026Documented thread of work
AprilConcurrent prompt/decode timing and task-quality measurement
June–AugustDeepSeek workstation-Blackwell compatibility and agent-stability bring-up
August 15–16Qwen3.8-27B engine/quantization bisects and adapter-version controls
August 23–24Multi-GPU communication correctness and host-staged workarounds
August 27Qwen3.8-Flash-Next MTP, batch sizing, memory headroom and quality checks
August 28 onwardThinking budgets, long-context behavior and creative-runtime experiments

Why I keep doing this locally

What draws me to local AI and open source is the ability to inspect the whole chain. I can preserve a working version, compare an adaptation against its base, examine a numerical failure, and build an agent around a task rather than a generic chat window.

Open weights, open-source software and a community-maintained kernel are different things; I do not assume every component has the same license or level of visibility. But the inspectable parts let me turn a failure into something reusable: a test, a correction, a safer default, or a recipe the next agent can follow.

My preferred performance metric is becoming time to a correct, accepted artifact under the workload I actually run. Decode speed still matters. So do the minutes before the first token, the evaluator that catches a bad result, and the recovery path when a long-running task goes wrong.

Questions about this note? Get in touch.

sipratt@gmail.com