<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yuri Pocepaev</title>
    <description>The latest articles on DEV Community by Yuri Pocepaev (@badbat4560).</description>
    <link>https://dev.to/badbat4560</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4145620%2Fa6ab932f-1ed2-48d1-8ce5-7137537e26a8.jpg</url>
      <title>DEV Community: Yuri Pocepaev</title>
      <link>https://dev.to/badbat4560</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/badbat4560"/>
    <language>en</language>
    <item>
      <title>How I Took GLiClass FP8 from 59 ms to 16 ms on an RTX 4050</title>
      <dc:creator>Yuri Pocepaev</dc:creator>
      <pubDate>Sun, 27 Sep 2026 14:40:04 +0000</pubDate>
      <link>https://dev.to/badbat4560/how-i-took-gliclass-fp8-from-59-ms-to-16-ms-on-an-rtx-4050-d1a</link>
      <guid>https://dev.to/badbat4560/how-i-took-gliclass-fp8-from-59-ms-to-16-ms-on-an-rtx-4050-d1a</guid>
      <description>&lt;p&gt;&lt;em&gt;Multilingual zero-shot classification on a 6 GB laptop GPU, with native FP8, Triton, CUDA Graphs, and reproducible measurements.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I quantized Knowledgator’s GLiClass Multilang Ultra into a custom FP8 W8A8 checkpoint. Then I wanted to find out how much of that compression could translate into faster local inference.&lt;/p&gt;

&lt;p&gt;The weights already took up a third less disk space. The first native FP8 implementation, however, needed about 59 ms per request. The original BF16 model took roughly 24 ms. Reducing precision had made the artifact smaller, but the execution path still needed work.&lt;/p&gt;

&lt;p&gt;After fusing auxiliary operations with Triton and adding CUDA Graphs, median request latency reached &lt;strong&gt;16.10 ms&lt;/strong&gt;. BF16 with the same graph wrapper measured &lt;strong&gt;23.79 ms&lt;/strong&gt;. On this particular workload, the optimized FP8 path was &lt;strong&gt;1.48× faster&lt;/strong&gt;, with roughly a third less memory allocated to tensors.&lt;/p&gt;

&lt;p&gt;Here is what the model does, what I changed, and what these measurements actually establish. The &lt;a href="https://huggingface.co/badbat4560/gliclass-multilang-ultra-fp8w8a8" rel="noopener noreferrer"&gt;weights, implementation, and benchmark report are on Hugging Face&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GLiClass does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/knowledgator/gliclass-multilang-ultra" rel="noopener noreferrer"&gt;GLiClass Multilang Ultra, developed by Knowledgator&lt;/a&gt;, classifies text against labels supplied at inference time. You can provide categories such as “delivery,” “refund,” “payment,” and “other” without training a new classification head for that particular list.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The courier has postponed my delivery for the second day.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivery&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refund&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful output is a class and its score. This makes classification a practical component before retrieval, application logic, or a more expensive model call.&lt;/p&gt;

&lt;p&gt;Potential applications include support-ticket routing, document categorization, intent detection, and filtering text collections. Those are examples of where this interface can help; this experiment did not validate every one of those tasks.&lt;/p&gt;

&lt;p&gt;I used the multilingual Ultra model. My quality evaluation covers English and Russian topic classification. It does not establish performance across every language supported by the upstream model or readiness for a particular production workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is inside the FP8 checkpoint?
&lt;/h2&gt;

&lt;p&gt;I started from the original GLiClass model and built the FP8 W8A8 quantization used here. A separate phase then audited the tensors, measured quality, and optimized its inference path with Triton and CUDA Graphs. Knowledgator remains the developer of the original model; the quantization and local inference work are my derivative contribution.&lt;/p&gt;

&lt;p&gt;The checkpoint quantizes &lt;strong&gt;168 matrices across 24 mT5 encoder blocks&lt;/strong&gt;: the attention projections &lt;code&gt;q&lt;/code&gt;, &lt;code&gt;k&lt;/code&gt;, &lt;code&gt;v&lt;/code&gt;, and &lt;code&gt;o&lt;/code&gt;, plus the feed-forward projections &lt;code&gt;wi_0&lt;/code&gt;, &lt;code&gt;wi_1&lt;/code&gt;, and &lt;code&gt;wo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Weights use FP8 E4M3 with a separate scale for each output channel. Activations are quantized dynamically for each token. Embeddings, normalization layers, and classification components remain in BF16.&lt;/p&gt;

&lt;p&gt;W8A8 therefore describes the selected linear projections. It does not mean every tensor in the model uses eight bits.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Weight file&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original BF16&lt;/td&gt;
&lt;td&gt;3,416,522,340 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8 W8A8&lt;/td&gt;
&lt;td&gt;2,259,902,516 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is a &lt;strong&gt;33.85% reduction&lt;/strong&gt;, or &lt;strong&gt;1.51× compression&lt;/strong&gt;. The shared embedding matrix alone accounts for approximately 1.02 GB of the remaining size.&lt;/p&gt;

&lt;p&gt;I also checked the unquantized tensors against a pinned snapshot of the original model. All 80 original tensors expected to remain unchanged matched byte for byte. The exact upstream revision used during the historical quantization was not recorded, so the report states that explicitly. The BF16 snapshot used for the comparison is pinned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why FP8 was initially slower
&lt;/h2&gt;

&lt;p&gt;The storage format does not tell you which operations the GPU will execute.&lt;/p&gt;

&lt;p&gt;One loading configuration reconstructed the weights in BF16 while retaining activation-quantization emulation. That path took around &lt;strong&gt;264 ms&lt;/strong&gt; per request. It was useful as a diagnostic, but it was not a native FP8 performance measurement.&lt;/p&gt;

&lt;p&gt;The next adapter used actual FP8 matrix multiplication through &lt;code&gt;torch._scaled_mm&lt;/code&gt; and cuBLAS. A separate profiler pass recorded &lt;strong&gt;168 FP8 GEMM kernels for sm89&lt;/strong&gt;, one for each replaced projection. Yet the complete request still took &lt;strong&gt;59.23 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The trace showed an obvious place to investigate: &lt;strong&gt;3,269 GPU kernel executions for one request&lt;/strong&gt;. Around the matrix multiplications, separate operations computed maxima, calculated scales, converted types, padded rows, and processed outputs.&lt;/p&gt;

&lt;p&gt;At batch size 1, the cost of running that sequence matters. Matrix multiplication is only part of request latency. The profile suggested reducing auxiliary operations and launch overhead; the subsequent measurements tested those changes at the request level.&lt;/p&gt;

&lt;h2&gt;
  
  
  The changes that made a difference
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fuse activation quantization
&lt;/h3&gt;

&lt;p&gt;For each token, the adapter needs an absolute maximum, a scale, and an FP8 representation of the activations. The original implementation expressed these as separate PyTorch operations.&lt;/p&gt;

&lt;p&gt;I moved them into one Triton kernel. It performs the reduction, calculates the scale, converts values to E4M3, and fills the additional rows needed for matrix alignment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fuse the output scaling
&lt;/h3&gt;

&lt;p&gt;The native FP8 GEMM produces an FP32 result. The adapter then applies the activation scale for each token and the weight scale for each output channel before converting back to BF16.&lt;/p&gt;

&lt;p&gt;That sequence became a second Triton kernel. The stored FP8 weights are used directly, without another round of weight quantization. GEMM continues to use &lt;code&gt;use_fast_accum=False&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Together, these changes reduced median latency to &lt;strong&gt;37.97 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replay the encoder with CUDA Graphs
&lt;/h3&gt;

&lt;p&gt;Next, I added CUDA Graphs around the encoder. A graph replays a captured sequence of GPU work, reducing the overhead of issuing its operations separately from Python.&lt;/p&gt;

&lt;p&gt;Text lengths vary, so the adapter uses four buckets: &lt;strong&gt;64, 128, 192, and 256 tokens&lt;/strong&gt;. Inputs are padded to the appropriate bucket, and the extra positions are masked. Before classification, the encoder output is sliced back to the original token count.&lt;/p&gt;

&lt;p&gt;Every request copies fresh tokens and a fresh mask into the graph's input buffers. Predictions are not cached. The output is cloned before it is returned to the caller.&lt;/p&gt;

&lt;p&gt;One compatibility issue needed attention: in the tested Transformers version, attention-mask construction copied a scalar from CPU to GPU during graph capture. Preparing the mask outside the captured region resolved it.&lt;/p&gt;

&lt;p&gt;With these changes, median latency reached &lt;strong&gt;16.10 ms&lt;/strong&gt;. The profiled request contained &lt;strong&gt;1,425 GPU kernel executions&lt;/strong&gt;, down from 3,269, while retaining all 168 native FP8 GEMMs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I measured it
&lt;/h2&gt;

&lt;p&gt;The machine was an &lt;strong&gt;NVIDIA RTX 4050 Laptop with 6 GB VRAM&lt;/strong&gt;, running Windows. The environment used Python 3.12.9, PyTorch 2.11.0 with CUDA 12.8, GLiClass 0.1.20, Transformers 5.14.1, and Triton Windows 3.6.0.post26.&lt;/p&gt;

&lt;p&gt;Latency was measured on one fixed AG News example with four candidate labels, at batch size 1. After warmup and quality evaluation, I ran 50 additional requests. Each request was synchronized with CUDA before and after timing. The measurement includes tokenization and postprocessing.&lt;/p&gt;

&lt;p&gt;Model loading, Triton compilation, and graph preparation are excluded from these steady-state timings. All four graph buckets were prepared in advance. FP8 graph preparation took &lt;strong&gt;2.18 seconds&lt;/strong&gt; in this run; that is not the complete cold-start time.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;Peak allocated tensor memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16, eager&lt;/td&gt;
&lt;td&gt;24.18 ms&lt;/td&gt;
&lt;td&gt;29.67 ms&lt;/td&gt;
&lt;td&gt;3.219 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BF16 + CUDA Graphs&lt;/td&gt;
&lt;td&gt;23.79 ms&lt;/td&gt;
&lt;td&gt;24.45 ms&lt;/td&gt;
&lt;td&gt;3.238 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8, initial native adapter&lt;/td&gt;
&lt;td&gt;59.23 ms&lt;/td&gt;
&lt;td&gt;66.36 ms&lt;/td&gt;
&lt;td&gt;2.145 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8 + Triton&lt;/td&gt;
&lt;td&gt;37.97 ms&lt;/td&gt;
&lt;td&gt;46.62 ms&lt;/td&gt;
&lt;td&gt;2.144 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FP8 + Triton + CUDA Graphs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16.10 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16.44 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.166 GiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fbadbat4560%2Fgliclass-multilang-ultra-fp8w8a8%2Fresolve%2Fmain%2Fbenchmarks%2Fbenchmark.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fbadbat4560%2Fgliclass-multilang-ultra-fp8w8a8%2Fresolve%2Fmain%2Fbenchmarks%2Fbenchmark.png" alt="GLiClass quality and latency on RTX 4050 Laptop" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the main comparison, I applied the same CUDA Graph wrapper to the original BF16 model. Optimized FP8 was &lt;strong&gt;1.48× faster than that control&lt;/strong&gt;. The &lt;strong&gt;3.68× improvement&lt;/strong&gt; compares the optimized implementation with my initial native FP8 adapter; it describes progress in the adapter implementation.&lt;/p&gt;

&lt;p&gt;The memory column measures allocated PyTorch tensors. It is different from total device memory shown by &lt;code&gt;nvidia-smi&lt;/code&gt;, which also reflects allocator reservations and other overhead. The current loader temporarily reconstructs weights in BF16 before replacing the projections, so &lt;strong&gt;2.17 GiB is not a loading-memory requirement&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Laptop clocks and thermals were not locked. These are measurements of this configuration, not performance confidence intervals across independent sessions. I did not measure sustained throughput at larger batch sizes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened to quality?
&lt;/h2&gt;

&lt;p&gt;The paired evaluation used &lt;strong&gt;664 identical examples&lt;/strong&gt;: a seeded subset of 256 AG News test examples and the full English and Russian SIB-200 test splits, with 204 examples each. SIB-200 candidate labels were in English for both languages.&lt;/p&gt;

&lt;p&gt;Dataset revisions, example indices, and hashes are recorded in the repository. The original and quantized tokenizers produced identical token IDs. No example was truncated at the 256-token limit.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dataset&lt;/th&gt;
&lt;th&gt;BF16 macro-F1&lt;/th&gt;
&lt;th&gt;Optimized FP8 macro-F1&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;79.08%&lt;/td&gt;
&lt;td&gt;79.49%&lt;/td&gt;
&lt;td&gt;+0.41 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIB-200 English&lt;/td&gt;
&lt;td&gt;84.57%&lt;/td&gt;
&lt;td&gt;84.04%&lt;/td&gt;
&lt;td&gt;−0.53 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIB-200 Russian&lt;/td&gt;
&lt;td&gt;84.09%&lt;/td&gt;
&lt;td&gt;83.42%&lt;/td&gt;
&lt;td&gt;−0.67 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Optimized FP8 changed &lt;strong&gt;5 of 664 top-1 predictions&lt;/strong&gt; relative to BF16, for &lt;strong&gt;99.25% agreement&lt;/strong&gt;. Agreement measures whether the two variants choose the same label; it is not classification accuracy. BF16 with and without the graph wrapper produced identical top-1 predictions on all 664 examples.&lt;/p&gt;

&lt;p&gt;I would not interpret the small positive AG News difference as an improvement in model quality. The other two datasets show declines, and this evaluation is too narrow to establish general quality preservation. A support-ticket router, for example, would need its own evaluation on representative tickets and candidate labels.&lt;/p&gt;

&lt;p&gt;The single-label pipeline retained only the winning label and its probability. Full score distributions were not saved, so this report makes no claims about changes across all logits or probability calibration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it locally
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://huggingface.co/badbat4560/gliclass-multilang-ultra-fp8w8a8" rel="noopener noreferrer"&gt;Hugging Face repository&lt;/a&gt; contains the weights, tokenizer, dependencies, adapter, and an &lt;code&gt;inference.py&lt;/code&gt; entry point.&lt;/p&gt;

&lt;p&gt;After downloading the repository and installing the environment described in the model card, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python inference.py &lt;span class="nt"&gt;--text&lt;/span&gt; &lt;span class="s2"&gt;"NASA launched a spacecraft to study Mars."&lt;/span&gt; &lt;span class="nt"&gt;--labels&lt;/span&gt; space politics sports business
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use the Python API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;inference&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_pipeline&lt;/span&gt;

&lt;span class="n"&gt;classifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_pipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inference_mode&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;classifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NASA launched a spacecraft to study Mars.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;space&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;politics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sports&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;business&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tested adapter supports &lt;strong&gt;sequential inference, batch size 1, and at most 256 input tokens&lt;/strong&gt;, including labels and prompt formatting. The tokenizer truncates longer inputs. Concurrent requests to one instance require synchronization or separate runners.&lt;/p&gt;

&lt;p&gt;This is currently a specialized inference path for the measured workload. Other GPUs, Linux execution, and installation in a fresh environment were not independently validated for this release. The measured configuration is Windows on an RTX 4050 Laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this experiment taught me
&lt;/h2&gt;

&lt;p&gt;A quantized checkpoint has at least three separate properties: its size on disk, its runtime memory use, and the speed of a particular implementation. Here, the smaller FP8 file initially produced a slower request. The latency improvement came from optimizing the operations around GEMM and the way the encoder's GPU work was launched.&lt;/p&gt;

&lt;p&gt;The result is a reproducible starting point: &lt;strong&gt;16.10 ms on the selected batch-1 request, approximately 2.17 GiB of allocated tensor memory, and measured quality changes&lt;/strong&gt;. Larger batches, longer inputs, and real application workloads are the next useful experiments. Their results need to be measured separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The main takeaway is simple: quantization gives you a smaller representation; making it faster is a separate engineering problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://huggingface.co/badbat4560/gliclass-multilang-ultra-fp8w8a8" rel="noopener noreferrer"&gt;model, source code, and report&lt;/a&gt; are available under Apache-2.0. Knowledgator developed the original model; this release provides an independent quantized representation, inference implementation, and evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;I’m Yuri Pocepaev, a software engineer and lead developer at &lt;a href="https://neuroprem.ru" rel="noopener noreferrer"&gt;Neuroprem&lt;/a&gt;. This article is part of my public engineering log.&lt;/p&gt;

</description>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Building an NVFP4 KV Cache for a Hybrid Qwen Model</title>
      <dc:creator>Yuri Pocepaev</dc:creator>
      <pubDate>Sun, 27 Sep 2026 14:33:27 +0000</pubDate>
      <link>https://dev.to/badbat4560/building-an-nvfp4-kv-cache-for-a-hybrid-qwen-model-l7d</link>
      <guid>https://dev.to/badbat4560/building-an-nvfp4-kv-cache-for-a-hybrid-qwen-model-l7d</guid>
      <description>&lt;p&gt;&lt;em&gt;Packing K/V into 576 bytes per token, fixing vLLM's hybrid cache planner, and measuring the result on an RTX PRO 6000 Blackwell.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I spent a fair amount of time getting NVFP4 KV storage working in my Qwen3.8-Flash-Next serving stack. The work covered the quantizer, a packed-cache writer, a sparse attention reader, and the vLLM integration needed to make them usable together.&lt;/p&gt;

&lt;p&gt;The most useful result from the latest comparison was capacity: at almost the same KV-cache memory budget, the server reported a pool of &lt;strong&gt;439,958 tokens with NVFP4, compared with 189,904 with BF16&lt;/strong&gt;. That is about &lt;strong&gt;2.32x&lt;/strong&gt; as many tokens in the planner's pool.&lt;/p&gt;

&lt;p&gt;Speed was less straightforward. BF16 was faster in the sequential test; NVFP4 finished the four-request batch faster. The small quality check produced the same aggregate classification score, but two answers changed. Those details matter just as much as the memory result.&lt;/p&gt;

&lt;p&gt;This is about quantizing the live attention cache. Both sides of the comparison use the same model checkpoint. I am not comparing newly quantized weights against an original BF16 model.&lt;/p&gt;

&lt;p&gt;The interesting integration failure happened before inference even started. My kernels used 576 bytes per token per full-attention layer, while an earlier planning step still accounted for 1,024. In a hybrid model, that disagreement was enough to prevent the server from allocating its cache.&lt;/p&gt;

&lt;p&gt;The code and evaluation artifacts are now public: &lt;a href="https://github.com/badbat4560/nvfp4-qsa" rel="noopener noreferrer"&gt;nvfp4-qsa on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the hybrid architecture matters
&lt;/h2&gt;

&lt;p&gt;The inspected configuration has 48 layers: three linear-attention layers followed by one full-attention layer, repeated twelve times. That gives 36 linear-attention layers and 12 full-attention layers. The latter use QSA sparse selection to choose positions for attention reads.&lt;/p&gt;

&lt;p&gt;The main QSA K/V arrays store information for individual tokens. The linear-attention path has recurrent state. This implementation compresses the former; recurrent state remains float32 in the observed launch configuration. Indexer state is another allocation with its own accounting.&lt;/p&gt;

&lt;p&gt;That distinction matters before discussing memory savings. A smaller K/V representation does not shrink the model weights, draft model, graph buffers or recurrent state by the same factor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed on the server
&lt;/h2&gt;

&lt;p&gt;On September 25, 2026 (UTC), I ran both cache modes in an isolated container during a maintenance window on the RTX PRO 6000 Blackwell. Production traffic was not routed to the test endpoint. Other services remained active, so this is a controlled comparison on a shared host.&lt;/p&gt;

&lt;p&gt;The checkpoint, MTP=3, maximum length of 169,984, twelve-sequence limit, 8,192-token scheduler budget, memory utilization 0.94 and float32 recurrent state were held constant. Compilation and CUDA graphs were enabled. The changed parameter was the cache format. NVFP4 ran first, followed by BF16; the order was not randomized or repeated.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cache&lt;/th&gt;
&lt;th&gt;Available KV budget&lt;/th&gt;
&lt;th&gt;Reported planner token pool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;5.85 GiB&lt;/td&gt;
&lt;td&gt;189,904&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;td&gt;5.84 GiB&lt;/td&gt;
&lt;td&gt;439,958&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is about 2.32 times the reported token pool at nearly the same KV budget. It is smaller than the 3.56x main-K/V format ratio because the hybrid model has other state and overhead. It is an allocator observation, not a sustained full-window concurrency test.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cache&lt;/th&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;Median TTFT&lt;/th&gt;
&lt;th&gt;Aggregate E2E output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;165.6 ms&lt;/td&gt;
&lt;td&gt;123.48 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;160.8 ms&lt;/td&gt;
&lt;td&gt;88.91 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1070.9 ms&lt;/td&gt;
&lt;td&gt;91.88 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;375.8 ms&lt;/td&gt;
&lt;td&gt;129.02 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each condition contains twelve requests for 128 output tokens and one measured batch. Throughput uses actual token usage and complete batch wall time. Two single-request warmups were performed; the four-way condition was not separately warmed. Unique user-message prefixes prevent identical requests, while shared chat-template prefixes can still be cached.&lt;/p&gt;

&lt;p&gt;BF16 was faster for the single-request stream in this run. NVFP4's four-way batch finished faster, but startup delays are visible and there are no independent repeated batches. Picking only the four-way result would overstate the performance evidence. The storage and planner-capacity benefit is the clearest result here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F86aec1cvzp6xwhmf2c3o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F86aec1cvzp6xwhmf2c3o.png" alt="Fresh server observations" width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Did the answers change?
&lt;/h2&gt;

&lt;p&gt;For quality I selected sixteen examples each from pinned AG News, SIB-200 English and SIB-200 Russian test files before evaluation. Both modes received the same questions and label order. Answers were scored by exact stripped label matching.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;NVFP4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;13/16&lt;/td&gt;
&lt;td&gt;13/16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIB-200 EN&lt;/td&gt;
&lt;td&gt;11/16&lt;/td&gt;
&lt;td&gt;12/16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIB-200 RU&lt;/td&gt;
&lt;td&gt;13/16&lt;/td&gt;
&lt;td&gt;12/16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Synthetic code retrieval&lt;/td&gt;
&lt;td&gt;9/9&lt;/td&gt;
&lt;td&gt;9/9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both modes scored 37/48 on classification, but their predictions were not identical. NVFP4 corrected one English example and changed the Russian version of the same example from correct to incorrect. Across all tasks, 55 of 57 answers matched. Equal aggregate accuracy is not proof that quality is unchanged.&lt;/p&gt;

&lt;p&gt;The nine retrieval questions placed one code at three positions in three text sizes. The maximum measured input was 9,280 tokens. This is a small regression check, not 120K retrieval validation or a full-dataset benchmark. Raw answers, token usage and dataset revisions are included. The chart's Wilson intervals expose the uncertainty from small subsets; translated examples also introduce dependence between the English and Russian observations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkh7k42kc24zlhndmq3zf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkh7k42kc24zlhndmq3zf.png" alt="Small quality subsets" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How 2,048 bytes became 576
&lt;/h2&gt;

&lt;p&gt;Each K or V vector has 256 elements. FP4 packs two elements into each byte, giving 128 data bytes. Groups of 16 elements each have one FP8 E4M3 scale, adding another 16 bytes. With two KV heads and both K and V, the total is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(256 / 2 + 256 / 16) x 2 x 2 = 576 bytes/token/layer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Main K/V representation&lt;/th&gt;
&lt;th&gt;Bytes per token per layer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;2,048&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8&lt;/td&gt;
&lt;td&gt;1,024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4, including group scales&lt;/td&gt;
&lt;td&gt;576&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At an equal main-K/V byte budget, that means 1.78 times as many tokens as FP8, or 3.56 times as many as BF16. Equivalently, the main-K/V allocation is 43.75% smaller than FP8. These are format calculations, not measured gains for the entire server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl6wxfpg4whwhx85afkfo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl6wxfpg4whwhx85afkfo.png" alt="Main K/V bytes per token" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The FP4 values use E2M1 with magnitudes &lt;code&gt;0, 0.5, 1, 1.5, 2, 3, 4, 6&lt;/code&gt; and a sign bit. Element &lt;code&gt;2j&lt;/code&gt; goes in the low nibble; element &lt;code&gt;2j+1&lt;/code&gt; goes in the high nibble. Decoding applies &lt;code&gt;fp4 * group_scale / global_scale&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The global scale must remain stable while older cache entries are live. Changing it when a new token arrives would change the interpretation of existing bytes unless the old scale were retained. The integrated cache initializes both K and V global scales to &lt;code&gt;1.0&lt;/code&gt;; each token carries its own group scales. Some standalone numerical tests instead choose a global scale from the tensor's absolute maximum. Their results cannot be treated as direct task-quality validation of the fixed-scale serving path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing and reading the packed cache
&lt;/h2&gt;

&lt;p&gt;The Triton writer takes BF16 values and a token-to-physical-slot mapping. It computes group maxima, converts scales to FP8, rounds values to E2M1 and packs pairs. Invalid slots do not write.&lt;/p&gt;

&lt;p&gt;The reader loads the selected QSA positions and their group scales and reconstructs values inside the attention kernel. It does not build a full-context BF16 staging cache.&lt;/p&gt;

&lt;p&gt;There is an important ownership contract: valid destination slots must be unique within a write call. Duplicate slots can cause conflicting writes. Standalone checks cover duplicate rejection and overwriting a physical slot with new data. The integrated scheduler must also satisfy this contract under append, prefix reuse, cancellation and preemption.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug between planning and allocation
&lt;/h2&gt;

&lt;p&gt;The hybrid allocator needed attention pages compatible with a recurrent-state page of 3,207,168 bytes. At an early planning step, the attention specification implied 1,024 bytes per token. A 3,136-token attention block was selected.&lt;/p&gt;

&lt;p&gt;Later, the physical allocation used the packed 576-byte representation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3,136 x 576 = 1,806,336 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That page was too small. The format could be correct and the read kernel could pass its tests while the server still could not allocate a valid hybrid cache.&lt;/p&gt;

&lt;p&gt;The correction belongs at the point where the backend tells the planner what it will actually allocate. The QSA backend's &lt;code&gt;customize_spec()&lt;/code&gt; now publishes packed content bytes before platform block-size adjustment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;packed_region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head_size&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head_size&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state_content_bytes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;packed_region&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is 288 bytes per KV head. The specification accounts for the two heads, producing 576 bytes per token. The matching page becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3,207,168 / 576 = 5,568 tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final cache uses physical uint8 K/V regions of 144 bytes and quantization mode &lt;code&gt;NONE&lt;/code&gt;. QSA owns the packing and unpacking; the finalized physical width must not be transformed again. The 5,568-token page is a consequence of this configuration, not a constant to copy into another model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking the implementation separately from model quality
&lt;/h2&gt;

&lt;p&gt;The earlier numerical checks below are from August 28, 2026 (UTC). I used an independent codec alongside the Python reference and CUDA quantizer. A separate comparison checks fused reading against dense attention over the same decoded NVFP4 values.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Historical check&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Independent decode vs Python reference&lt;/td&gt;
&lt;td&gt;cosine 0.99999726&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Independent codec vs CUDA packed bytes&lt;/td&gt;
&lt;td&gt;0.999634 match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fused vs dense NVFP4 read&lt;/td&gt;
&lt;td&gt;cosine 0.99999702&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fused NVFP4 vs BF16 oracle&lt;/td&gt;
&lt;td&gt;cosine approximately 0.99079&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read boundary matrix&lt;/td&gt;
&lt;td&gt;27/27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write boundary matrix&lt;/td&gt;
&lt;td&gt;28/28&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The byte comparisons are not universally exact. Against the Python reference, 123 of 567 differing nibbles were exact midpoint ties. The remaining differences involved values near rounding boundaries and scale arithmetic. Calling all differences “just tie-breaking” would hide part of the observation.&lt;/p&gt;

&lt;p&gt;The fused-versus-dense comparison checks arithmetic and layout on the quantized representation. The comparison against BF16 includes quantization error. Neither number measures downstream task accuracy.&lt;/p&gt;

&lt;p&gt;The archived Compute Sanitizer runs report zero errors in four modes on the tested small shapes. Larger functional boundary cases are a different gate. This does not prove every long-context serving path or concurrent execution pattern safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the long-context run actually tells me
&lt;/h2&gt;

&lt;p&gt;Before the paired comparison, I had also exercised the integrated server with a synthetic 120,064-token input. In that August run, a single request reached its first text chunk in 14.569 seconds; a group of seven took 101.953 seconds to finish. Each request generated only one output token.&lt;/p&gt;

&lt;p&gt;That established that the tested path could process a long input. The prompt repeated a phrase, so it said little about whether the model could retrieve useful information from that context. The September retrieval check is more meaningful for correctness, but only reaches 9,280 input tokens.&lt;/p&gt;

&lt;p&gt;The August run also used different settings, including MTP=2 rather than 3. I keep its raw observations in the package as a separate record; they are not a before-and-after speed comparison with the September results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration problems that remain
&lt;/h2&gt;

&lt;p&gt;Two earlier BF16 startup attempts with MTP disabled failed with a CUDA illegal memory access, including in eager mode. A standalone GDN warmup with the same head dimensions passed on Blackwell and did not locate the earlier asynchronous fault. The successful comparison above uses MTP=3. FP8 was not measured in this window.&lt;/p&gt;

&lt;p&gt;The serving stack also needed invalid/padded expert-ID handling, workspace reuse between compatible sequential MoE layers, and separation of the target's NVFP4 B12x backend from its BF16 draft backend. Embedding, output-head and index-buffer references required their own fixes.&lt;/p&gt;

&lt;p&gt;Those changes are packaged separately. Sharing scratch buffers is appropriate only when their uses do not overlap or are explicitly synchronized. CPU tests verify route masking and shared-object identity; they do not establish CUDA stream or graph safety for arbitrary overlapping wrappers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the cache without loading the model
&lt;/h2&gt;

&lt;p&gt;I separated the writer, reader and independent codec into a Python package that does not import vLLM. That makes the format and page operations testable without loading the full model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;nvfp4_qsa&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PackedCache&lt;/span&gt;

&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PackedCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The checked API rejects duplicate valid write slots, non-finite inputs and incompatible dimensions. Its validation synchronizes with the host, so this wrapper is not presented as a production fast path. It explicitly rejects CUDA graph capture. Raw-kernel callers still own stream ordering and cache lifetime.&lt;/p&gt;

&lt;p&gt;The separately installed wheel passed 16 tests on an RTX 4050 Laptop: nine CPU tests and seven GPU cases. GPU coverage includes ordinary, zero, small and saturating values; slot reuse; invalid metadata; and sparse attention over fragmented pages compared with an independent CPU calculation using decoded cache values.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can be reproduced today
&lt;/h2&gt;

&lt;p&gt;The complete server measurements came from my model-specific vLLM build. Its historical base revision has not been verified against a public upstream commit, which limits how directly someone else can reproduce that full stack.&lt;/p&gt;

&lt;p&gt;I have also prepared a narrow port against public vLLM revision &lt;code&gt;e126687a9a828d513c01a07cd69f025f27d63280&lt;/code&gt;, where the model implementation uses the module name &lt;code&gt;qwen4_exp&lt;/code&gt;. The patch applies cleanly and its BF16/NVFP4 physical-spec checks pass. &lt;strong&gt;I have not validated a full server run of that public port.&lt;/strong&gt; The server benchmark numbers in this article belong to the tested model-specific build.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/badbat4560/nvfp4-qsa" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; contains the installable portable source, tests, the integration patch, raw request results, dataset revisions and scripts for reconstructing the evaluation prompts. It contains no model weights. Large checkpoint tensors were not rehashed during the maintenance window; the provenance record distinguishes download metadata from verified file hashes.&lt;/p&gt;

&lt;p&gt;The next serving experiments need repeated, independently warmed batches, a matched FP8 baseline, and a larger quality evaluation. The failed MTP-disabled startup path also needs investigation. Those are separate pieces of work from validating the cache format.&lt;/p&gt;

&lt;p&gt;For now, the strongest result is the extra room reported by the allocator, backed by working reads and writes and a small paired evaluation. The part I would carry into another implementation is the physical-byte accounting: the quantizer, reader and cache planner all need to agree on what a page actually contains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code and reproduction
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/badbat4560/nvfp4-qsa" rel="noopener noreferrer"&gt;Source code and quick start&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/badbat4560/nvfp4-qsa/blob/main/docs/installation.md" rel="noopener noreferrer"&gt;Installation and API example&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/badbat4560/nvfp4-qsa/blob/e820978345932cc9518f1101771d0bb0d0755b07/docs/current-evaluation.md" rel="noopener noreferrer"&gt;Evaluation protocol and limitations&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/badbat4560/nvfp4-qsa/tree/e820978345932cc9518f1101771d0bb0d0755b07/results/2026-09-25" rel="noopener noreferrer"&gt;Raw paired benchmark results&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/badbat4560/nvfp4-qsa/blob/main/integration/README.md" rel="noopener noreferrer"&gt;Public vLLM integration status&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Running Qwen Flash-Next NVFP4 in vLLM: PLE Loading, B12x Fixes, and Stable Inference</title>
      <dc:creator>Yuri Pocepaev</dc:creator>
      <pubDate>Sun, 27 Sep 2026 14:32:29 +0000</pubDate>
      <link>https://dev.to/badbat4560/running-qwen-flash-next-nvfp4-in-vllm-ple-loading-b12x-fixes-and-stable-inference-dc8</link>
      <guid>https://dev.to/badbat4560/running-qwen-flash-next-nvfp4-in-vllm-ple-loading-b12x-fixes-and-stable-inference-dc8</guid>
      <description>&lt;p&gt;Companion code: &lt;a href="https://github.com/badbat4560/qwen-flash-runtime-notes" rel="noopener noreferrer"&gt;PLE and B12x compatibility patches, tests, and deployment notes&lt;/a&gt;. The repository contains extracted fixes and validation notes, not a complete runtime image.&lt;/p&gt;

&lt;p&gt;The model loaded. The health endpoint returned a successful response. A few requests completed. Then concurrent traffic arrived, and the inference process ran out of GPU memory.&lt;/p&gt;

&lt;p&gt;That was the most instructive part of bringing Qwen Flash-Next NVFP4 online. There was no single switch that made the deployment work. We had to get a large checkpoint onto disk, adapt a loader to its tensor layout, and find a memory configuration that survived actual generation. Each milestone removed one obstacle while exposing the next.&lt;/p&gt;

&lt;p&gt;The eventual result was a server configured for 131,072 tokens of context, with a limit of 16 sequences, that completed three rounds of 16 concurrent requests without a recorded container restart. Getting there required giving up some of our initial context and concurrency targets—and being precise about what the successful test actually demonstrated.&lt;/p&gt;

&lt;p&gt;Before getting into the details, a big shout-out to &lt;strong&gt;&lt;a href="https://huggingface.co/windowsxp811203" rel="noopener noreferrer"&gt;windowsxp811203&lt;/a&gt;&lt;/strong&gt;. The quality of the work and the detailed reports deserve real credit. It is great to see someone put that much care into both the result and its documentation.&lt;/p&gt;

&lt;p&gt;This account follows the initial bfloat16 KV-cache baseline and the additional B12x integration work needed by the developed serving stack. The measured 48-request test belongs to the initial baseline. The later B12x work is described separately, without transferring that test result to a different runtime. The loader and backend patches were required independently of KV-cache quantization.&lt;/p&gt;

&lt;p&gt;The first practical problem was simply getting the checkpoint onto the machine. The recorded download comprised 31 files, approximately 186.4 GB in total. A single PLE shard accounted for roughly 102.4 GB. With artifacts of that size, storage preparation and download recovery were part of the deployment work rather than incidental setup.&lt;/p&gt;

&lt;p&gt;We provisioned a dedicated volume with an ext4 filesystem and used the Hugging Face CLI with eight download workers. The transfer ran through a local proxy. At one point, progress stalled at around 14 GB. After restarting, the reported progress fell to approximately 4.7 GB before the transfer resumed at about 93 MiB/s.&lt;/p&gt;

&lt;p&gt;The practical consequence was extra transfer time and uncertainty about completion. A progress counter was useful for watching the download, but it was not sufficient evidence that the checkpoint was ready. We waited for the final shard to finish before moving on to loading.&lt;/p&gt;

&lt;p&gt;It is also important to keep disk size separate from GPU residency. The total checkpoint size was not a prediction of the amount of VRAM the process would consume. This deployment used PLE CPU offload, so some of the storage and loading work belonged on the host side. Later failures still depended on the memory available on the GPU during generation.&lt;/p&gt;

&lt;p&gt;Before replacing the existing model server, we saved its container configuration and retained a rollback target. That gave the experiments a defined starting point and preserved a route back if the new model failed. It also prevented the previous deployment from becoming another variable while we were changing the new one.&lt;/p&gt;

&lt;p&gt;The initial runtime was a model-specific vLLM container image. It provided a starting point, but it did not accept this checkpoint without local changes. Two incompatibilities surfaced before inference tuning could begin.&lt;/p&gt;

&lt;p&gt;The first was in the attention configuration. Twelve layer entries used &lt;code&gt;qwen_sparse_attention&lt;/code&gt;, while the runtime path we were using expected &lt;code&gt;full_attention&lt;/code&gt;. We supplied a local configuration override that changed those entries.&lt;/p&gt;

&lt;p&gt;That was an operational compatibility change, and it has an important limit: getting past configuration parsing does not establish that the two settings are computationally equivalent. This work did not include a controlled comparison of output quality or attention behavior before and after the override. The evidence supports the narrower claim that the override allowed this runtime to proceed with loading.&lt;/p&gt;

&lt;p&gt;The second incompatibility involved the layout of the PLE embedding table. The loader expected checkpoint shards with numeric suffixes. The checkpoint instead exposed the complete table through a tensor name whose shard index was empty. There was also a naming mismatch: the loader path had to recognize both the longer prefixed form and the shorter form actually encountered during loading.&lt;/p&gt;

&lt;p&gt;The distinction sounds small until the tensor behind the name is enormous. A loader cannot safely infer that an unrecognized name represents an interchangeable piece of the model. It needs to identify the layout and validate the shape before copying the data.&lt;/p&gt;

&lt;p&gt;Our fix went through three iterations. The first recognized the empty suffix and tried to hand the tensor to the ordinary loading path. That was insufficient. The second added explicit handling for the complete table, including shape validation, but recognized only the longer prefix. The third also handled the shorter name used by the checkpoint. That was the version that loaded successfully.&lt;/p&gt;

&lt;p&gt;The successful path checked the incoming tensor against the expected full embedding dimensions. It then used the embedding-copy helper with a checkpoint offset of zero and the relevant embedding partition boundaries. Numbered shards retained their existing handling, and other weights continued through the regular loader.&lt;/p&gt;

&lt;p&gt;The shape check was an essential part of the change. Supporting an additional serialization layout should not mean accepting arbitrary tensor dimensions just because the name looks familiar. A mismatch should fail at loading, where the error can be explained, rather than being allowed to become an inference problem later.&lt;/p&gt;

&lt;p&gt;We packaged the patch as a small layer over the original container image, replacing a single loader module. Keeping that change narrow made it easier to understand what differed between the starting runtime and the working one. The rest of the deployment could then be investigated without treating the entire runtime as a custom fork.&lt;/p&gt;

&lt;p&gt;The PLE loader was only one part of the runtime work. The serving stack subsequently required a separate set of fixes at the vLLM–FlashInfer boundary to use the B12x MoE path on our Blackwell SM120 GPU. This is a separate stage from the initial concurrency test reported below; its patches should not be retroactively attributed to that earlier run.&lt;/p&gt;

&lt;p&gt;That distinction changes how the deployment should be described. A single loader replacement explains the initial checkpoint-loading fix. The more developed serving image also carries backend integration, speculative-decoding, offload, and scheduling changes. Reproducing that stack requires preserving the complete working runtime, rather than assuming that a stock image plus the PLE patch is equivalent.&lt;/p&gt;

&lt;p&gt;The B12x work addressed two very different problems: invalid expert routes and duplicated workspace allocations. We changed the integration around the backend. We did not rewrite the B12x CUDA kernels.&lt;/p&gt;

&lt;p&gt;During dummy profiling and padding, the integration could pass an expert ID of &lt;code&gt;-1&lt;/code&gt;. That value represented an inactive route on the caller's side, but it could reach a path that treated it as an expert index. Our deployment notes associate this failure with &lt;code&gt;cudaErrorIllegalAddress&lt;/code&gt;, GPU-process failures, and Xid 31 reports.&lt;/p&gt;

&lt;p&gt;The wrapper now validates each selected expert ID before calling the original backend. A route is valid only when its ID is nonnegative and below the expert count. An invalid route is redirected to expert zero and its routing weight is set to zero. Valid routes retain both their IDs and weights.&lt;/p&gt;

&lt;p&gt;The essential operation is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expert_ids&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expert_ids&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;num_experts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;safe_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expert_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;safe_weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routing_weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redirecting an invalid ID alone would be wrong: it would turn padding into a real contribution from expert zero. Clearing the routing weight is what preserves the intended zero contribution of an inactive route. The wrapper also catches IDs above the valid range, although that defensive handling is not a substitute for diagnosing an upstream routing bug if one appears outside padding or profiling.&lt;/p&gt;

&lt;p&gt;The saved unit test makes the transformation explicit. Given IDs &lt;code&gt;[-1, 7, 512]&lt;/code&gt; for a model with 512 experts, the wrapper passes &lt;code&gt;[0, 7, 0]&lt;/code&gt;. Given routing weights &lt;code&gt;[0.4, 0.5, 0.6]&lt;/code&gt;, it passes &lt;code&gt;[0.0, 0.5, 0.0]&lt;/code&gt;. That verifies the wrapper's treatment of invalid routes; it is not, by itself, a numerical validation of the complete GPU inference path.&lt;/p&gt;

&lt;p&gt;The second problem was workspace ownership. Our deployment notes describe allocations of roughly 400 MiB per MoE layer across 48 layers. At that scale, allocating an independent workspace for every layer can consume approximately 18.75 GiB before considering the rest of the runtime. This is an estimate from the reported per-layer allocation, not a separately measured memory saving.&lt;/p&gt;

&lt;p&gt;Those layers execute sequentially in the execution pattern for which the patch was built. Compatible wrappers can therefore reuse scratch storage instead of reserving a separate copy for every layer.&lt;/p&gt;

&lt;p&gt;The implementation caches three buffers: the static workspace, the dynamic workspace, and the MoE output buffer. The cache key includes expert count, top-k, hidden and intermediate dimensions, maximum token count, local expert count, output dtype, device, activation, quantization mode, and source format. Wrappers with matching keys reuse the same buffers; a different key triggers a separate allocation.&lt;/p&gt;

&lt;p&gt;Compatibility and execution order both matter. Matching dimensions alone would not make shared writable storage safe for overlapping executions. In this deployment, reuse relies on the sequential layer execution pattern. A change to scheduling or stream behavior would require checking that assumption again.&lt;/p&gt;

&lt;p&gt;The saved workspace test verifies that two identically configured wrappers allocate once and share the same buffer objects. That is useful evidence about the Python integration. It does not replace a runtime test under CUDA graphs and real traffic.&lt;/p&gt;

&lt;p&gt;This design is also reflected in the current &lt;a href="https://docs.flashinfer.ai/api/fused_moe.html" rel="noopener noreferrer"&gt;FlashInfer B12x API documentation&lt;/a&gt;, which exposes optional shared static, dynamic, and output buffers for compatible wrappers. That describes the current upstream interface; it does not establish that the runtime version used in our deployment already provided an equivalent integration.&lt;/p&gt;

&lt;p&gt;Why pursue B12x at all? Our deployment notes identify Marlin as the fallback and B12x as the preferred native execution path for the NVFP4 MoE workload on this GPU. This article does not include a controlled comparison between the two, so we do not attach a speedup number or a claim of identical output quality to that choice. Selecting a backend does not, by itself, requantize the checkpoint, but numerical equivalence still needs measurement.&lt;/p&gt;

&lt;p&gt;The surrounding runtime also needed coordinated choices for CUDA graph shapes, prefill budget, and MTP fallback. The recorded constraints included the &lt;code&gt;2^31 - 1&lt;/code&gt; memref limit in the affected path, autotuner allocations, and a narrow VRAM margin. These were additional integration constraints, not all manifestations of the same memory bug. The memref limit in particular should not be read as a universal limit on GPU memory or model context.&lt;/p&gt;

&lt;p&gt;This is why the custom development runtime mattered. Sanitizing routes addressed invalid indexing. Workspace reuse addressed duplicate buffers. Graph, prefill, and speculative-decoding choices addressed other execution constraints. The PLE loader change solved a separate checkpoint-format problem. Replacing the full image with a stock runtime would discard that combination of fixes in the environment described here.&lt;/p&gt;

&lt;p&gt;The resulting stack combines separate responsibilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NVFP4 MoE experts
  -&amp;gt; FlashInfer B12x
  -&amp;gt; route validation and compatible workspace reuse

PLE n-gram table
  -&amp;gt; CPU offload and checkpoint-layout compatibility

KV cache
  -&amp;gt; an independent precision and capacity decision
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These patches were necessary for checkpoint compatibility and backend execution in our environment, before any decision to quantize the KV cache. A reproduction should preserve the complete working runtime and validate each change against that baseline.&lt;/p&gt;

&lt;p&gt;With those later integration changes distinguished from the initial baseline, the earlier memory-tuning sequence is easier to read on its own terms.&lt;/p&gt;

&lt;p&gt;Once the checkpoint loaded, memory became the main constraint. The initial goal was a context window close to 240,000 tokens with a relatively generous sequence limit. Our first configuration requested 245,760 tokens, used &lt;code&gt;gpu-memory-utilization=0.948&lt;/code&gt;, and allowed up to 64 sequences.&lt;/p&gt;

&lt;p&gt;It failed the KV-cache capacity check. The runtime required 6.85 GiB and had 6.79 GiB available. Its calculated maximum was 243,936 tokens, just below the requested context length.&lt;/p&gt;

&lt;p&gt;That small difference encouraged a tempting adjustment: increase the memory-utilization setting. We tried 0.96. In this environment, that left too little physical headroom for other work during initialization, and problems appeared around autotuning and CUDA graphs. The setting increased the budget available to the runtime, but it did not create additional memory on the device.&lt;/p&gt;

&lt;p&gt;Reducing the requested context to 240,000 while returning utilization to 0.948 got us further. The runtime reported KV capacity for 242,909 tokens, completed startup, and answered its health check.&lt;/p&gt;

&lt;p&gt;Then the first real inference failed. The failing operation wanted another 192 MiB, but only 32.44 MiB was free.&lt;/p&gt;

&lt;p&gt;At that point the distinction between startup success and inference success was concrete. The server had enough memory for the checks it had already performed. It did not have enough for an allocation reached during generation.&lt;/p&gt;

&lt;p&gt;The full progression looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Maximum context&lt;/th&gt;
&lt;th&gt;Memory utilization&lt;/th&gt;
&lt;th&gt;Maximum sequences&lt;/th&gt;
&lt;th&gt;Observed result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;245,760&lt;/td&gt;
&lt;td&gt;0.948&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;Failed KV-capacity validation: 6.85 GiB required, 6.79 GiB available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;245,760&lt;/td&gt;
&lt;td&gt;0.96&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;Insufficient headroom around autotuning and CUDA graphs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;240,000&lt;/td&gt;
&lt;td&gt;0.948&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;Started successfully, then failed during generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200,000&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;With expandable segments enabled, failed during startup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200,000&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;Without that allocator setting, passed sequential requests but failed under concurrent traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;131,072&lt;/td&gt;
&lt;td&gt;0.92&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;Completed three rounds of 16 concurrent requests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The inference failure pointed into the GDN linear-attention path. The trace identified a temporary state-tensor allocation during forward execution. That allocation needed free memory beyond the resources already accounted for by the successful startup and KV-capacity checks.&lt;/p&gt;

&lt;p&gt;The GPU was also shared with other workloads. Its nominal capacity was therefore not the same as the budget available to this model. The amount of free memory at a critical allocation depended on more than the model weights and configured KV cache.&lt;/p&gt;

&lt;p&gt;Our working explanation was that the deployment had too little margin for temporary allocations during generation, and the concurrent workload exposed that margin more reliably than isolated requests. The failure locations and reported allocation sizes supported that explanation. We did not collect a full allocation-level memory profile, so we could not quantify every contributor to the peak.&lt;/p&gt;

&lt;p&gt;The next configuration was particularly misleading. At 200,000 tokens of context, utilization of 0.94, and a sequence limit of 32, several sequential requests succeeded. Requests through the API gateway also completed. If the acceptance criterion had been “the endpoint answers,” we could have stopped there.&lt;/p&gt;

&lt;p&gt;Concurrent traffic produced another OOM. This time the failing allocation requested 290 MiB with only 150 MiB free. The container restarted, making it clear that the earlier successful responses had not exercised the workload that mattered.&lt;/p&gt;

&lt;p&gt;That result changed the way we evaluated subsequent attempts. A successful sequential request remained useful as a functional check, but it could no longer stand in for a concurrency test. We needed to send overlapping requests and observe the process while generation was in progress.&lt;/p&gt;

&lt;p&gt;We also tried the allocator option &lt;code&gt;expandable_segments&lt;/code&gt;. In this setup, it introduced a different failure mode: an OOM during memory mapping, followed by a &lt;code&gt;pidfd_getfd: Operation not permitted&lt;/code&gt; error in the PLE offload worker. We removed the setting.&lt;/p&gt;

&lt;p&gt;That experiment did not establish that expandable segments are generally unsuitable for inference. It established that the option did not solve this deployment's problem and made this particular startup path fail. Carrying it forward would have added another unresolved variable.&lt;/p&gt;

&lt;p&gt;The configuration that passed our concurrent test reduced three limits together: maximum context, maximum sequences, and memory utilization. The resulting settings were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max-model-len: 131072
kv-cache-dtype: bfloat16
gpu-memory-utilization: 0.92
max-num-seqs: 16
PLE CPU offload: enabled
MTP speculative tokens: 1
prefix caching: enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime also used the Qwen reasoning and tool-call parsers. Multimodal request limits were configured, but the test described below used text requests; it did not validate image or video workloads.&lt;/p&gt;

&lt;p&gt;Changing three memory-related parameters at once means we cannot attribute the success to one of them in isolation. We did not perform an exhaustive sweep that held every other factor constant. The objective at this stage was to find a usable baseline in the actual environment. The combination above supplied that baseline for the workload we ran.&lt;/p&gt;

&lt;p&gt;We tested it with three rounds of 16 concurrently submitted chat-completion requests. Each request asked for a roughly 300-word essay about distributed systems. The generation limit was 700 tokens, temperature was 0.8, and the client timeout was 180 seconds. Each round finished before the next one began.&lt;/p&gt;

&lt;p&gt;The script counted responses containing a &lt;code&gt;finish_reason&lt;/code&gt; field and inspected the container restart count. It also sampled GPU memory approximately every two seconds during the beginning of each round.&lt;/p&gt;

&lt;p&gt;The recorded results were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Round&lt;/th&gt;
&lt;th&gt;Responses with a finish reason&lt;/th&gt;
&lt;th&gt;Recorded container restarts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;16 / 16&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;16 / 16&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;16 / 16&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;48 / 48&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The highest sampled GPU memory usage was &lt;strong&gt;92,712 MiB&lt;/strong&gt;, approximately &lt;strong&gt;90.54 GiB&lt;/strong&gt;. The final memory snapshot reported &lt;strong&gt;4,537 MiB free&lt;/strong&gt;, approximately &lt;strong&gt;4.43 GiB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Those numbers need a little care. The samples describe device-wide usage, including other processes on the GPU. Sampling at intervals can miss short peaks, and the script did not continuously sample for the entire duration of every request. The free-memory reading was a final snapshot; it should not be presented as a continuously guaranteed reserve under load.&lt;/p&gt;

&lt;p&gt;The completion count also has a specific meaning. All 48 responses passed the script's check for the presence of a finish reason. That check did not evaluate the quality of the essays or require that every response ended naturally rather than reaching its token limit. We did not derive a throughput claim, a latency percentile, or a quality score from this test.&lt;/p&gt;

&lt;p&gt;Most importantly, a server configured for 131,072 tokens of context was not being tested with 131,072-token prompts. These were short prompts with bounded generation. The result established that the selected configuration handled this concurrent text workload without a recorded restart. Testing long-context prefill, mixed prompt lengths, sustained traffic, and multimodal requests would require additional runs.&lt;/p&gt;

&lt;p&gt;After the concurrent test, requests through the API gateway also completed. That checked a separate part of the deployment: the model was reachable through the intended serving route after the container changes. It complemented the direct inference test, while leaving the same workload limits in place.&lt;/p&gt;

&lt;p&gt;What made this exercise useful was the sequence of increasingly meaningful checks. Download completion established that the artifacts were present. Successful loading established that the runtime could interpret them. A health response established that the service was up. A generated response exercised inference. Concurrent requests exposed a memory problem that the earlier checks had missed.&lt;/p&gt;

&lt;p&gt;The same distinction applied to the fixes. The loader patch addressed a checkpoint-layout mismatch. The configuration override got past a runtime compatibility issue but still needed separate quality validation. The final memory settings addressed the observed failures under a particular workload. None of those changes, on its own, justified calling every aspect of the deployment validated.&lt;/p&gt;

&lt;p&gt;We ended with a smaller context target than we initially wanted, a lower sequence limit, and a configuration that completed the chosen concurrency check. That was a much more useful foundation for further experiments than a larger advertised context attached to a process that restarted when traffic overlapped.&lt;/p&gt;

&lt;p&gt;Further experiments need separate baselines for the runtime and the representation being changed. Long-context and sustained-load tests should exercise the complete custom stack. Any KV-cache quantization experiment should retain the established backend integration and be evaluated independently. Keeping those changes distinguishable lets us measure memory consumption, reliability, and answer quality without attributing a result to the wrong patch.&lt;/p&gt;

</description>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
