<?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: Wayne</title>
    <description>The latest articles on DEV Community by Wayne (@wheynelau).</description>
    <link>https://dev.to/wheynelau</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%2F3898242%2F77ad6a26-606a-4f53-a83c-55494768faf9.jpeg</url>
      <title>DEV Community: Wayne</title>
      <link>https://dev.to/wheynelau</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wheynelau"/>
    <language>en</language>
    <item>
      <title>Measuring LLM Prefix Caching: The Cache Hit Rate Metric</title>
      <dc:creator>Wayne</dc:creator>
      <pubDate>Wed, 05 Aug 2026 02:49:08 +0000</pubDate>
      <link>https://dev.to/wheynelau/measuring-llm-prefix-caching-the-cache-hit-rate-metric-2n9m</link>
      <guid>https://dev.to/wheynelau/measuring-llm-prefix-caching-the-cache-hit-rate-metric-2n9m</guid>
      <description>&lt;p&gt;Prefix caching is one of the biggest cost levers in LLM serving. vLLM, SGLang, TGI, and most hosted providers all do some version of it: during prefill they compute a key-value (KV) cache, and if a later request shows up with the same prompt prefix, they reuse that cache instead of recomputing it. Done well, a lot of expensive prefill compute turns into a cheap cache lookup.&lt;/p&gt;

&lt;p&gt;Whether it helps depends on how much of your traffic re-sends the same prefix, and most benchmarking runs don't tell you. This is part of my &lt;a href="https://wheynelau.dev/posts/2025-12-15-benchmarking-performance/" rel="noopener noreferrer"&gt;LLM benchmarking guide&lt;/a&gt;. Here I want to focus on how to actually measure cache effectiveness: the metric, why it matters for agentic workloads, and the cost angle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What prefix caching reuses
&lt;/h2&gt;

&lt;p&gt;During prefill, the server computes the KV cache for the input prompt. If a later request sends a prefix the server has already seen, it can skip recomputing that part and just serve it from cache. The classic example is a multi-turn conversation: each turn re-sends the entire prior history, and ideally everything except the newest user message comes back from cache.&lt;/p&gt;

&lt;p&gt;This is also why cache reuse only shows up in multi-turn or shared-prefix workloads. A single isolated request has nothing to reuse. Turn 0 is always a cold start. So if you want to measure caching, you have to re-send history, which means multi-turn requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters most for agentic workloads
&lt;/h2&gt;

&lt;p&gt;A chat conversation re-sends some history every turn, but an agentic coding loop does this on fast-forward, and at a scale where caching stops being optional and starts dominating both latency and cost.&lt;/p&gt;

&lt;p&gt;Here's how a coding agent actually runs (Claude Code, Cursor, Cline, that sort of thing). It loops: read the task, decide on an action, call a tool to read a file or run a command, get the result back, decide the next action, call another tool. Each one of those iterations is a new API request, and every request re-sends the &lt;em&gt;entire accumulated context&lt;/em&gt;. The system prompt, the original task, all the prior reasoning, every previous tool call and its result. The only genuinely new content is the latest tool result and the model's next decision. Everything before that is a prefix the server has already computed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency.&lt;/strong&gt; Without caching, each step's time-to-first-token includes recomputing prefill for the whole context. As the conversation grows, every step gets a little slower, so the agent loop itself drags as the session wears on. With good caching, only the new tool result triggers prefill, and TTFT stays roughly flat across the session instead of climbing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Prefill compute scales with context length, so re-paying for the full context on every tool call gets expensive fast. Agentic sessions are notoriously long, often tens of thousands of tokens and well past 100k. With caching you pay for each token's prefill once instead of on every subsequent step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So an agentic session is really just a long multi-turn conversation where history gets re-sent every turn, which is exactly the case &lt;code&gt;cache_hit_rate&lt;/code&gt; was built for. If you're picking a serving setup for agentic workloads, cache hit rate under a realistic multi-turn load is one of the most telling numbers you can collect.&lt;/p&gt;

&lt;p&gt;One nuance worth knowing: some providers also offer &lt;em&gt;explicit&lt;/em&gt; prompt caching, where the client marks cache breakpoints (Anthropic's &lt;code&gt;cache_control&lt;/code&gt; is the example). That's a different mechanism from the automatic prefix caching most OpenAI-compatible endpoints do, and llmperf-rs only measures the automatic kind. For a standard tool-call loop against a vLLM-style endpoint, automatic prefix caching is what applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cache hit rate metric
&lt;/h2&gt;

&lt;p&gt;The metric I use measures cache reuse against the content that was &lt;em&gt;previously sent&lt;/em&gt;, not the whole request. Caching only reuses what the server has already seen: the assistant's prior outputs and earlier user prompts that get echoed back in the next request. New tokens in the current turn can never be cached, because the server hasn't seen them before.&lt;/p&gt;

&lt;p&gt;Implemented in &lt;a href="https://github.com/wheynelau/llmperf-rs" rel="noopener noreferrer"&gt;llmperf-rs&lt;/a&gt;:&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;cache_hit_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_tokens_of_non_final_turns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Numerator:&lt;/strong&gt; the sum of &lt;code&gt;cached_tokens&lt;/code&gt; reported by the endpoint on each turn, read from &lt;code&gt;prompt_tokens_details.cached_tokens&lt;/code&gt; in the streamed &lt;code&gt;usage&lt;/code&gt; object. &lt;code&gt;None&lt;/code&gt; means the endpoint didn't report the field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Denominator:&lt;/strong&gt; the sum of each turn's total tokens (input + output) for every turn &lt;em&gt;except the last turn&lt;/em&gt;. The last turn's content is never re-sent in a later request, so it can never be served from cache and is excluded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;100% means every previously-sent token came back from cache. In a perfect cache, &lt;code&gt;cached_tokens&lt;/code&gt; equals the prior-turn total on every warm turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge cases
&lt;/h2&gt;

&lt;p&gt;A few that bite in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single-turn runs report &lt;code&gt;None&lt;/code&gt;.&lt;/strong&gt; With only one turn there's no history to re-send, so the denominator is zero. Cache hit rate is a multi-turn concept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An all-&lt;code&gt;None&lt;/code&gt; run reports &lt;code&gt;None&lt;/code&gt;.&lt;/strong&gt; If the endpoint never reports &lt;code&gt;cached_tokens&lt;/code&gt;, you get &lt;code&gt;None&lt;/code&gt;, not zero. That's deliberate: &lt;code&gt;None&lt;/code&gt; means "not measurable", which is different from &lt;code&gt;0.0&lt;/code&gt; (a cache that's just never hit).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A mismatched endpoint dilutes instead of nulling.&lt;/strong&gt; If some turns report &lt;code&gt;cached_tokens&lt;/code&gt; and others don't, the unobserved turns are left out of the numerator but their re-sent history still counts in the denominator. So a noisy endpoint just pulls the ratio down rather than wiping it out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold start contributes nothing.&lt;/strong&gt; Turn 0 reports &lt;code&gt;cached_tokens = 0&lt;/code&gt; or just omits it, so it doesn't move the numerator either way.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to run it
&lt;/h2&gt;

&lt;p&gt;You need multi-turn requests, which in llmperf-rs is &lt;code&gt;--multi-turn N&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000/v1   &lt;span class="c"&gt;# vLLM with prefix caching enabled&lt;/span&gt;
llmperf &lt;span class="nt"&gt;--model&lt;/span&gt; Qwen/Qwen3-4B-Instruct-2507 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--multi-turn&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--max-num-completed-requests&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The summary then includes a &lt;code&gt;cache_hit_rate&lt;/code&gt; field (alongside the TTFT/ITL/throughput metrics covered in the &lt;a href="https://wheynelau.dev/posts/2025-12-15-benchmarking-performance/" rel="noopener noreferrer"&gt;main guide&lt;/a&gt;):&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Example value only, illustrative and not from a real run.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"cache_hit_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.91&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single summary number aggregates across the whole run. Per-turn &lt;code&gt;cached_tokens&lt;/code&gt; and &lt;code&gt;turn_index&lt;/code&gt; are also written to the individual-responses file if you want to see how the cache builds up over turns after the cold start.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on reasoning content
&lt;/h2&gt;

&lt;p&gt;There's a subtlety if you're benchmarking reasoning models. The common guidance is to discard a model's &lt;code&gt;reasoning_content&lt;/code&gt; from the message history you send back, to save tokens. llmperf-rs does the opposite for multi-turn runs: it echoes the previous turn's &lt;code&gt;reasoning_content&lt;/code&gt; on the assistant message.&lt;/p&gt;

&lt;p&gt;The reason is exactly this topic. Providers that support prefix caching over reasoning (Z.ai's "Preserved thinking" with &lt;code&gt;clear_thinking: false&lt;/code&gt;, for example) can reuse the KV cache across turns only if the reasoning is re-sent. Dropping it to save on echoed-input tokens throws away the cache reuse, which usually costs more than it saves. Providers that don't understand &lt;code&gt;reasoning_content&lt;/code&gt; just ignore the field, so it's safe to send.&lt;/p&gt;

&lt;p&gt;So if you're measuring cache hit rate on a reasoning model, make sure you're re-sending the reasoning. Otherwise you're measuring a workload that disables its own cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Token-count accuracy matters less here than elsewhere&lt;/strong&gt;, because the ratio is between two token sums rather than a token count against a wall-clock time. Chat-template token variance affects numerator and denominator similarly, so it mostly cancels out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache behavior depends on server config&lt;/strong&gt;, not just the model. vLLM's prefix caching can be on or off; KV-cache size and eviction policy affect whether a warm turn actually hits cache. A low cache hit rate under load can point at preemption rather than a broken cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This measures endpoint-level prefix caching&lt;/strong&gt;, not GPU-level cache statistics. For kernel-level breakdowns you'd want a tool like aiperf or trtllm-bench. See my notes on &lt;a href="https://wheynelau.dev/posts/2026-08-01-llmperf-alternatives/" rel="noopener noreferrer"&gt;llmperf alternatives&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;If you've enabled prefix caching, cache hit rate is how you confirm it's earning its keep. The key thing to get right is the denominator: measure cache reuse against the history you re-sent, not against the whole request, or you'll understate a cache that's working fine. And remember it's strictly a multi-turn metric. A single-turn benchmark tells you nothing about caching.&lt;/p&gt;

&lt;p&gt;The full version with the exact math and more detail is on &lt;a href="https://wheynelau.dev/posts/2026-08-01-measuring-llm-prefix-caching/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>benchmarking</category>
      <category>performance</category>
    </item>
    <item>
      <title>llmperf Is Archived: Alternatives for LLM Benchmarking</title>
      <dc:creator>Wayne</dc:creator>
      <pubDate>Wed, 05 Aug 2026 02:47:21 +0000</pubDate>
      <link>https://dev.to/wheynelau/llmperf-is-archived-alternatives-for-llm-benchmarking-2hae</link>
      <guid>https://dev.to/wheynelau/llmperf-is-archived-alternatives-for-llm-benchmarking-2hae</guid>
      <description>&lt;p&gt;If you've been using &lt;a href="https://github.com/ray-project/llmperf" rel="noopener noreferrer"&gt;ray-project/llmperf&lt;/a&gt;, you may have noticed it's now in archive mode. No new updates, no fixes, no responses to issues. If you're evaluating it for the first time, that's worth knowing before you build anything on top of it.&lt;/p&gt;

&lt;p&gt;This page is part of my &lt;a href="https://wheynelau.dev/posts/2025-12-15-benchmarking-performance/" rel="noopener noreferrer"&gt;LLM benchmarking guide&lt;/a&gt;, which covers the metrics themselves (TTFT, ITL, throughput). Here I want to focus on the tools — what's out there now that llmperf is effectively done, and what I ended up building.&lt;/p&gt;

&lt;h2&gt;
  
  
  What llmperf was good at
&lt;/h2&gt;

&lt;p&gt;Credit where it's due — llmperf was the go-to open-source option for benchmarking OpenAI-compatible endpoints. It measured the metrics that matter (TTFT, ITL, throughput), handled concurrency, and came out of the Anyscale/Ray team, so it had credibility. For a lot of teams it did the job well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I started looking around
&lt;/h2&gt;

&lt;p&gt;Two things pushed me to look at alternatives, and both are about fit rather than flaws:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ITL aggregation.&lt;/strong&gt; llmperf computes Inter-Token Latency by averaging within each request first, then aggregating those per-request averages. That's a reasonable choice and works well for many use cases. But I was specifically trying to catch latency &lt;em&gt;spikes&lt;/em&gt; during the decode phase, and per-request averaging smooths exactly those out. I needed the raw distribution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Startup overhead.&lt;/strong&gt; Runs spawn Ray workers, so there's a meaningful spin-up cost before the first request fires. When I just want to poke at an endpoint quickly, that's more ceremony than I want — I was after something closer to curl than a cluster setup.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither is a flaw. They're design decisions that matched llmperf's goals and didn't match mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The alternatives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Single binary / low deps&lt;/th&gt;
&lt;th&gt;GPU-level metrics&lt;/th&gt;
&lt;th&gt;Distributed&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/ai-dynamo/aiperf" rel="noopener noreferrer"&gt;aiperf&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Python package, very comprehensive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.vllm.ai/en/latest/benchmarking/cli/" rel="noopener noreferrer"&gt;vllm-bench&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;vLLM-specific&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/NVIDIA/TensorRT-LLM" rel="noopener noreferrer"&gt;trtllm-bench&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;TensorRT-LLM specific&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/guidellm/guidellm" rel="noopener noreferrer"&gt;GuideLLM&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Strong reporting and dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/wheynelau/llmperf-rs" rel="noopener noreferrer"&gt;llmperf-rs&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Single Rust binary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There's also a note for genai-perf: NVIDIA sunsetted it and moved development to aiperf.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use which
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You need GPU-level metrics&lt;/strong&gt; (prefix caching, kernel-level breakdown) → aiperf or trtllm-bench. This is where llmperf-rs won't help you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're testing vLLM-specific behavior&lt;/strong&gt; → vllm-bench.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want dashboards and visual reporting&lt;/strong&gt; → GuideLLM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need distributed load generation&lt;/strong&gt; → aiperf.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You just want to hit an OpenAI-compatible endpoint quickly, with minimal setup, and see TTFT/ITL/throughput&lt;/strong&gt; → llmperf-rs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the gap I was trying to fill — something I could drop onto a box and run in seconds, that preserved raw ITL values so spikes weren't hidden.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built: llmperf-rs
&lt;/h2&gt;

&lt;p&gt;llmperf-rs is a single Rust binary that benchmarks any OpenAI-compatible endpoint (vLLM, Ollama, local APIs). It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keeps &lt;strong&gt;raw ITL values&lt;/strong&gt; across all responses before computing percentiles, so p99 and max actually reflect spikes.&lt;/li&gt;
&lt;li&gt;Uses &lt;strong&gt;API-reported token counts&lt;/strong&gt; from the &lt;code&gt;usage&lt;/code&gt; field when available, falling back to a tokenizer you specify. The original llmperf used one tokenizer for everything, which gets inaccurate across model families.&lt;/li&gt;
&lt;li&gt;Outputs console summaries plus JSON for digging in with pandas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not trying to compete with the GPU-deep tools. I think of it as one level above curl — fast to start, low dependency, good enough for most "how's this endpoint doing" questions.&lt;/p&gt;

&lt;p&gt;If that trade-off sounds right for you: grab it from the &lt;a href="https://github.com/wheynelau/llmperf-rs/releases" rel="noopener noreferrer"&gt;releases page&lt;/a&gt;, or &lt;code&gt;cargo install --git https://github.com/wheynelau/llmperf-rs&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;llmperf being archived doesn't mean the tooling disappeared — aiperf, vllm-bench, GuideLLM, and others are all actively maintained. The choice mostly comes down to how deep you need to go (GPU metrics vs endpoint metrics) and how much setup you're willing to tolerate.&lt;/p&gt;

&lt;p&gt;You can find this post and more on &lt;a href="https://wheynelau.dev/posts/2026-08-01-llmperf-alternatives/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>benchmarking</category>
      <category>llm</category>
    </item>
    <item>
      <title>Switching to Secondary Is Faster</title>
      <dc:creator>Wayne</dc:creator>
      <pubDate>Sat, 02 May 2026 06:57:07 +0000</pubDate>
      <link>https://dev.to/wheynelau/switching-to-secondary-is-faster-mhc</link>
      <guid>https://dev.to/wheynelau/switching-to-secondary-is-faster-mhc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Remember, switching to your pistol is always faster than reloading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same idea applies to LLM workflows.&lt;/p&gt;

&lt;p&gt;Most of the time, you don't need a flagship model to scaffold a project. Boilerplate, spec drafts, and initial plans are all tasks where a smaller model can do the heavy lifting. Then you pass the result to a larger model for review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works
&lt;/h2&gt;

&lt;p&gt;Prefill is usually a single forward pass (not counting advanced stuff like chunking and sequence parallelism). Fundamentally, the next token is just &lt;code&gt;model.forward()&lt;/code&gt;. How does this help?&lt;/p&gt;

&lt;p&gt;Say your initial prompt is 16k tokens (a rough ballpark for a Claude Code session) and you need to generate another 16k tokens of output (tool calls, reads, edits included). If your large model generates at 50t/s, a small model can easily hit 200t/s. That's 80 seconds versus 320 seconds for the same 16k tokens.&lt;/p&gt;

&lt;p&gt;The concept is the same as speculative decoding. Modern decoders use a small draft model to propose multiple tokens at once, then the large model verifies them in parallel. Using a secondary model for the first pass is just speculative decoding scaled to 16k tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;Here's what I've been doing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Plan&lt;/strong&gt; — either with a small model for speed, or directly with a large model for precision. The large model is more accurate but spends more tokens on the planning stage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review&lt;/strong&gt; — pass the plan to a large model, fix what's wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate code&lt;/strong&gt; — small model implements from the refined spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review again&lt;/strong&gt; — catch what the small model missed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the small model, I've been using &lt;code&gt;Qwen 3.6 35b MoE&lt;/code&gt;. It's fast enough to run locally and produces reasonable boilerplate. The large model then acts as a reviewer rather than a first-pass generator.&lt;/p&gt;

&lt;p&gt;This hasn't been tested on novel codebases. For truly new problems, I write the code myself and use the small model for repetitive tasks like generating tests and boilerplate.&lt;/p&gt;

&lt;p&gt;You can find this post and more on &lt;a href="https://wheynelau.dev/posts/2026-05-02-switching-to-secondary-is-faster/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>agenticcoding</category>
      <category>workflow</category>
    </item>
    <item>
      <title>Ansible at Home</title>
      <dc:creator>Wayne</dc:creator>
      <pubDate>Mon, 27 Apr 2026 05:17:16 +0000</pubDate>
      <link>https://dev.to/wheynelau/ansible-at-home-1ig5</link>
      <guid>https://dev.to/wheynelau/ansible-at-home-1ig5</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;While some concepts in Engineering should not be brought back to home, I find that Ansible was one of the few tools that is actually useful in a home environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Ansible?
&lt;/h2&gt;

&lt;p&gt;Ansible is a powerful automation tool that can help manage and configure systems efficiently. Another key important aspect commonly missed out is documentation. In the past, I would usually SSH into my home server and make changes directly. If I remember to document it, I would save code snippets into a README.md or obsidian note. However, this approach is prone to human error and can lead to inconsistencies over time. Most forms of IaC (Infrastructure as Code) tools are self documenting, as the code itself serves as documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup and Configuration
&lt;/h2&gt;

&lt;p&gt;Before diving into use cases, it's important to set up Ansible properly for a home environment. The configuration is straightforward and makes running playbooks much more convenient.&lt;/p&gt;

&lt;p&gt;I keep two files in the project directory: an &lt;code&gt;ansible.cfg&lt;/code&gt; pointing to my inventory file and enabling &lt;code&gt;become_ask_pass&lt;/code&gt; so it prompts for sudo passwords rather than storing credentials (security first, even at home), and an &lt;code&gt;inventory.ini&lt;/code&gt; with at least &lt;code&gt;localhost ansible_connection=local&lt;/code&gt; so playbooks run locally without SSH overhead. With those in place, &lt;code&gt;ansible-playbook playbook.yml&lt;/code&gt; just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  System configuration
&lt;/h3&gt;

&lt;p&gt;I am using a consumer intel CPU with a stock cooler for my homelab. As I don't expect it to run heavy workloads, I don't need it to run at full power. I set the PL1 and PL2 power limits through Ansible rather than using the &lt;code&gt;intel-undervolt&lt;/code&gt; tool. This way, if I ever need to re-install the OS or set up a new server, I can easily apply the same configuration without having to remember the exact commands or settings.&lt;/p&gt;

&lt;p&gt;The playbook validates that PL1 and PL2 values fall within acceptable ranges (hard lower/upper limits) and ensures PL2 &amp;gt;= PL1 before applying them. It then writes to the sysfs powercap interfaces to set sustained and burst power limits, and creates a systemd service for persistence across reboots.&lt;/p&gt;

&lt;p&gt;It's easy to make mistakes when setting raw power values -- adding one extra zero can be disastrous. With Ansible, I can specify values like 65W and 90W instead of 65000000 and 90000000, and the validation layer catches out-of-range inputs before they get applied.&lt;/p&gt;

&lt;h3&gt;
  
  
  Restic setup
&lt;/h3&gt;

&lt;p&gt;Restic is a great backup tool that can be used to back up data to various locations. The backup scripts are manually written, but the cron jobs and log rotation are managed by Ansible. If the below process were to be done manually, it would be prone to human error and inconsistencies, as multiple files are involved, such as cron jobs, log rotation configuration, and the backup scripts themselves.&lt;/p&gt;

&lt;p&gt;The playbook ensures restic is installed, makes the backup scripts executable, sets up two daily cron jobs (one for immich data at 2 AM, one for documents at 3 AM), and configures logrotate to keep 21 days of compressed logs with daily rotation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As you add more services and configurations to your home environment, the benefits of using Ansible become even more apparent. It helps maintain consistency, reduces the risk of human error, and serves as documentation for your setup. Whether you're managing a single server or multiple devices, Ansible can streamline your home automation tasks effectively.&lt;/p&gt;

&lt;p&gt;The full version with the complete playbook examples is on &lt;a href="https://wheynelau.dev/posts/2025-08-12-ansible-at-home/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ansible</category>
      <category>homelab</category>
      <category>iac</category>
    </item>
    <item>
      <title>Making Compression a Habit with zstd</title>
      <dc:creator>Wayne</dc:creator>
      <pubDate>Sun, 26 Apr 2026 13:47:27 +0000</pubDate>
      <link>https://dev.to/wheynelau/making-compression-a-habit-with-zstd-2gie</link>
      <guid>https://dev.to/wheynelau/making-compression-a-habit-with-zstd-2gie</guid>
      <description>&lt;p&gt;With zstd being added to &lt;a href="https://docs.python.org/3/library/compression.zstd.html" rel="noopener noreferrer"&gt;Python 3.14&lt;/a&gt;, I've been using compressed files more often in my workflow. Here's what I've learned about making compression a habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python Data Processing with Compression
&lt;/h2&gt;

&lt;p&gt;Python 3.14 adds native &lt;code&gt;zstd.open()&lt;/code&gt; support, which is a big step forward. Here's the comparison:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before 3.14&lt;/strong&gt; (with &lt;code&gt;zstandard&lt;/code&gt; package):&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;zstandard&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;zstd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;

&lt;span class="c1"&gt;# Writing compressed JSONL with Zstandard
&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;Alice&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;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;95&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;Bob&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;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;87&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;Charlie&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;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;92&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Write
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data.jsonl.zst&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;wb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zstd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ZstdCompressor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&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;cctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream_writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Read
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data.jsonl.zst&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;rb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;dctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zstd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ZstdDecompressor&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;dctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream_reader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;text_stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TextIOWrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text_stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&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;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Python 3.14+&lt;/strong&gt; is much simpler:&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;compression&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;zstd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="c1"&gt;# Read and print first record
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;zstd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data.jsonl.zst&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;rt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;  &lt;span class="c1"&gt;# Remove break to read all lines
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API mirrors regular &lt;code&gt;open()&lt;/code&gt; -- just use &lt;code&gt;zstd.open()&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;'wt'&lt;/code&gt; mode for writing text, &lt;code&gt;'rt'&lt;/code&gt; for reading&lt;/li&gt;
&lt;li&gt;Typical compression ratio: 6-7x size reduction at &lt;code&gt;zstd-3&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Benchmarking Your Workload
&lt;/h2&gt;

&lt;p&gt;You should benchmark compression according to your workload to determine your trade-offs.&lt;/p&gt;

&lt;p&gt;For archival of logs or long-term storage, you can use higher compression levels of &lt;code&gt;zstd&lt;/code&gt;. Archives like Pushshift Reddit typically use level 22. For most use cases, &lt;code&gt;zstd-3&lt;/code&gt; is a good default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with Compressed Files
&lt;/h2&gt;

&lt;p&gt;Zstd includes tools for viewing, searching, and processing compressed files without manual decompression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick commands:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;zstdcat data.json.zst&lt;/code&gt; -- view the contents&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zstdless data.json.zst&lt;/code&gt; -- page through like &lt;code&gt;less&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zstdgrep "error" events.json.zst&lt;/code&gt; -- search inside compressed files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zstdgrep -c "timeout" events.json.zst&lt;/code&gt; -- count occurrences&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also pipe to other tools: &lt;code&gt;zstdcat events.json.zst | grep ERROR | jq '.timestamp'&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Transferring files
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Rsync
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;-z&lt;/code&gt; flag compresses data during transfer. On highly compressible files, rsync may report &lt;code&gt;speedup &amp;gt; 1.0x&lt;/code&gt;. Here's a test with about 66GB of JSONL files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sent 50,322 bytes  received 12,451,737,167 bytes  19,290,143.28 bytes/sec
total size is 66,857,841,487  speedup is 5.37
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That speedup means the data sent was much less than the original file size. This is highly beneficial if you're network bound or concerned about egress costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 and Cloud Storage
&lt;/h3&gt;

&lt;p&gt;AWS charges for outbound data transfer (egress). Compressing data before storage can significantly reduce these costs. With a 7.0x compression ratio, a $14,000 egress bill drops to roughly $2,000.&lt;/p&gt;

&lt;p&gt;Here's an upload comparison on a gigabit connection with a 4GB JSONL file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Compressed upload (zstd -k -c ... | s5cmd pipe)&lt;/span&gt;
real    0m8.139s
&lt;span class="c"&gt;# Result in S3: 363.7MB&lt;/span&gt;

&lt;span class="c"&gt;# Uncompressed upload (s5cmd cp)&lt;/span&gt;
real    0m57.547s
&lt;span class="c"&gt;# Result in S3: 4.0GB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same principle works for downloading: &lt;code&gt;s5cmd cat s3://bucket/data.zst | zstd -d &amp;gt; data.jsonl&lt;/code&gt;. Compression takes longer than decompression, but the speedup is usually worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I use &lt;code&gt;zstdcat&lt;/code&gt; to read files and rarely need to edit them in an IDE. This habit cut my text storage by up to 80%. There's a balance between convenience, speed, and storage - and this works for me. More optimized formats like protobuf or arrow exist, but most text processing still uses JSON.&lt;/p&gt;

&lt;p&gt;The full version with code examples and benchmarks is on &lt;a href="https://wheynelau.dev/posts/compression-with-ztsd/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>linux</category>
      <category>performance</category>
      <category>compression</category>
    </item>
    <item>
      <title>Using hf tokenizers in Rust</title>
      <dc:creator>Wayne</dc:creator>
      <pubDate>Sun, 26 Apr 2026 13:43:42 +0000</pubDate>
      <link>https://dev.to/wheynelau/using-hf-tokenizers-in-rust-1k5p</link>
      <guid>https://dev.to/wheynelau/using-hf-tokenizers-in-rust-1k5p</guid>
      <description>&lt;p&gt;The &lt;code&gt;tokenizers&lt;/code&gt; library from Hugging Face provides an efficient way to work with text tokenization in Rust. This guide shows you how to get started with pretrained tokenizers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;First, add the tokenizer library to your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo add tokenizers &lt;span class="nt"&gt;--features&lt;/span&gt; http,hf-hub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Basic Usage
&lt;/h2&gt;

&lt;p&gt;Here's a complete example that loads a pretrained tokenizer and processes text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tokenizers&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Send&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Sync&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Load a pretrained tokenizer&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hf-internal-testing/llama-tokenizer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"This is a sample string to tokenize"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Encode the text (false = no special tokens)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="nf"&gt;.encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Get token IDs&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;token_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="nf"&gt;.get_ids&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Token IDs: {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_ids&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Get token text&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="nf"&gt;.get_tokens&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tokens: {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Original: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Number of tokens: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_ids&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="nf"&gt;.decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Original: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Decoded: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Working with Different Models
&lt;/h2&gt;

&lt;p&gt;You can use various pretrained models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GPT-2 tokenizer&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;gpt_tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// BERT tokenizer&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bert_tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bert-base-uncased"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Llama tokenizer&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;llama_tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hf-internal-testing/llama-tokenizer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;To change the cache directory for downloaded models, set the &lt;code&gt;HF_HOME&lt;/code&gt; environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HF_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/your/cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting environment variables programmatically is not recommended as it requires an unsafe block. &lt;/p&gt;

&lt;h3&gt;
  
  
  Private Repositories
&lt;/h3&gt;

&lt;p&gt;If you encounter this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Error: RequestError&lt;span class="o"&gt;(&lt;/span&gt;Status&lt;span class="o"&gt;(&lt;/span&gt;401, Response[status: 401, status_text: Unauthorized, url: https://huggingface.co/google/gemma-3-12b-it/resolve/main/tokenizer.json]&lt;span class="o"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It means you are not authenticated and may require a token. There are two ways to achieve this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write your token to $HF_HOME/token, usually $HOME/.cache/huggingface&lt;/li&gt;
&lt;li&gt;Within Rust code:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tokenizers&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FromPretrainedParameters&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FromPretrainedParameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;your very secret token&amp;gt;"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="nn"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"google/gemma-3-4b-it"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note that you may still need to get permission to access the repos.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Branches
&lt;/h3&gt;

&lt;p&gt;You can specify a specific branch or revision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tokenizers&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FromPretrainedParameters&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FromPretrainedParameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;  &lt;span class="c1"&gt;// or specific commit hash&lt;/span&gt;
    &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="nn"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"google/gemma-3-4b-it"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  User-Agent
&lt;/h3&gt;

&lt;p&gt;Params have another variable called &lt;code&gt;user_agent&lt;/code&gt; for customizing the HTTP client user agent string.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tokenizers&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FromPretrainedParameters&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FromPretrainedParameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-rust-app/1.0"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="nn"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The Hugging Face &lt;code&gt;tokenizers&lt;/code&gt; library provides a robust, production-ready solution for text processing in Rust applications. With support for pretrained models, authentication for private repositories, and flexible configuration options, it's an excellent choice for NLP workflows in Rust.&lt;/p&gt;

&lt;p&gt;You can find this post and more on &lt;a href="https://wheynelau.dev/posts/2025-11-21-tokenizer-rust/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>machinelearning</category>
      <category>nlp</category>
      <category>huggingface</category>
    </item>
    <item>
      <title>Setting Up Docker CI for Rust with cargo-dist</title>
      <dc:creator>Wayne</dc:creator>
      <pubDate>Sun, 26 Apr 2026 13:29:54 +0000</pubDate>
      <link>https://dev.to/wheynelau/setting-up-docker-ci-for-rust-with-cargo-dist-36nn</link>
      <guid>https://dev.to/wheynelau/setting-up-docker-ci-for-rust-with-cargo-dist-36nn</guid>
      <description>&lt;h1&gt;
  
  
  Rust CI
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;Building Rust inside Docker is slow. A typical multi-stage Dockerfile compiles the binary in one stage and copies it into a minimal image in another. That works fine for local builds, but in CI it takes a long time, especially when you're emulating arm64 through QEMU.&lt;/p&gt;

&lt;p&gt;The better approach: let cargo-dist handle the compilation as part of the release workflow. By the time the Docker job runs, the binaries are already built and available as GitHub Actions artifacts. Docker just copies them in. QEMU is still needed for the final multi-arch manifest, but it's only moving files around rather than running a compiler through emulation, so arm64 builds don't take nearly as long.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;The starting point is the &lt;a href="https://axodotdev.github.io/cargo-dist/book/quickstart/rust.html" rel="noopener noreferrer"&gt;cargo-dist quickstart guide&lt;/a&gt;. Once that's in place, you need a few configuration pieces to trigger the Docker build after the release.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;release.yml&lt;/code&gt;, add a &lt;code&gt;custom-docker-publish&lt;/code&gt; job that calls your docker-publish workflow and passes the plan output and binary name as inputs.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;dist-workspace.toml&lt;/code&gt;, set &lt;code&gt;post-announce-jobs&lt;/code&gt; to point at your docker workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;post-announce-jobs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"./docker-publish"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;github-custom-job-permissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;"docker-publish"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;packages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;contents&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"read"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;allow-dirty&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"ci"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The permissions block was needed because my docker workflow didn't have enough access by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Docker Workflow
&lt;/h2&gt;

&lt;p&gt;The workflow runs as a &lt;code&gt;workflow_call&lt;/code&gt; and takes the dist plan JSON, binary name, and target triple suffix as inputs. Here's the overall structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="na"&gt;binary_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="na"&gt;target_triple_suffix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown-linux-musl"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The job itself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set up QEMU and Docker Buildx&lt;/li&gt;
&lt;li&gt;Log in to GHCR&lt;/li&gt;
&lt;li&gt;Extract the version from the dist plan's &lt;code&gt;announcement_tag&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Generate Docker metadata (semver tags, major.minor, major, and latest for non-prereleases)&lt;/li&gt;
&lt;li&gt;Download the amd64 and arm64 artifacts produced by cargo-dist&lt;/li&gt;
&lt;li&gt;Extract and normalize the artifacts, moving binaries into the right folders&lt;/li&gt;
&lt;li&gt;Build and push with &lt;code&gt;docker/build-push-action@v6&lt;/code&gt; targeting both &lt;code&gt;linux/amd64&lt;/code&gt; and &lt;code&gt;linux/arm64&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The version tags are pulled from the dist plan, so they stay in sync with cargo-dist's release process. The &lt;code&gt;latest&lt;/code&gt; tag is skipped for prereleases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v6&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;platforms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64,linux/arm64&lt;/span&gt;
    &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.meta.outputs.tags }}&lt;/span&gt;
    &lt;span class="na"&gt;build-args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BINARY_NAME=${{ inputs.binary_name }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Dockerfile
&lt;/h2&gt;

&lt;p&gt;The Dockerfile depends on what your binary needs. I used distroless images and determined the right base image by running &lt;code&gt;ldd&lt;/code&gt; on the compiled binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;linux-vdso.so.1 &lt;span class="o"&gt;(&lt;/span&gt;0x00007ffdfb764000&lt;span class="o"&gt;)&lt;/span&gt;
libgcc_s.so.1 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /lib/x86_64-linux-gnu/libgcc_s.so.1
libm.so.6 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /lib/x86_64-linux-gnu/libm.so.6
libc.so.6 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /lib/x86_64-linux-gnu/libc.so.6
/lib64/ld-linux-x86-64.so.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since this binary needed libc and libm, I went with &lt;code&gt;gcr.io/distroless/cc-debian13:nonroot&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/cc-debian13:nonroot&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; TARGETARCH&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BINARY_NAME&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chmod=755 artifacts/${TARGETARCH}/${BINARY_NAME} /usr/local/bin/app&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; nonroot:nonroot&lt;/span&gt;

&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/usr/local/bin/app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full version with the complete workflow YAML and more context is on &lt;a href="https://wheynelau.dev/posts/2026-02-06-rust-ci/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>docker</category>
      <category>ci</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Learnings of the Poor</title>
      <dc:creator>Wayne</dc:creator>
      <pubDate>Sun, 26 Apr 2026 06:44:18 +0000</pubDate>
      <link>https://dev.to/wheynelau/learnings-of-the-poor-2086</link>
      <guid>https://dev.to/wheynelau/learnings-of-the-poor-2086</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Necessity is the mother of invention&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was already GPU poor, but a recent job change combined with rising component prices have also made me RAM and NVMe poor.&lt;/p&gt;

&lt;p&gt;While I am nowhere close to the experts of optimisations in the early 2000s or 90s, I took this time to brush up on some fundamentals and key concepts in Python. As the saying goes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Premature optimisation is the root of all evil"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We are not looking for very deep level optimisations, these changes aim to follow the Pareto Principle where 80% of the outcome comes from 20% of the effort. The changes below may or may not be 20% effort but I would consider them low-effort.&lt;/p&gt;

&lt;p&gt;As such, there won't be any discussion on performance profiling, where we are determining hot loops, cache misses, memory reallocations etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Iterators
&lt;/h2&gt;

&lt;p&gt;Frankly I think this is an important concept that has a great carryover regardless of languages. Understanding iterators also helps if you need to think of channels, which is very important in Go.&lt;/p&gt;

&lt;p&gt;The typical approach collects results at every stage into lists:&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;first_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;second_processing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;write_processed_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output.jsonl&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 issue: if &lt;code&gt;data.jsonl&lt;/code&gt; is bigger than your RAM, you run OOM very fast. Using &lt;code&gt;yield&lt;/code&gt; instead keeps memory usage low:&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;collections.abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;first_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_good&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each function in the pipeline takes an &lt;code&gt;Iterator[dict]&lt;/code&gt; and yields records one at a time. Memory usage drops significantly.&lt;/p&gt;

&lt;p&gt;Caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Files are held open throughout the pipeline, so unintentional edits or moves will break it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;json.dumps&lt;/code&gt; does not add a trailing newline, so &lt;code&gt;f.write(json.dumps(record) + '\n')&lt;/code&gt; is intentional when writing JSONL.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Learning points
&lt;/h3&gt;

&lt;p&gt;I find that iterators are a step before understanding pipelines, channels, or pub/sub patterns. When you understand iterators, you understand the bottlenecks of your code. They are fundamentally all iterators that consume and yield.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;process_data&lt;/code&gt; is slow (1 line per second) while reading and filtering is fast (4 lines per second), the pipeline is bounded by 1 line per second. The solution is more processing workers bridged through queues or channels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read-worker-1 -&amp;gt; Filter-worker-1 -&amp;gt; Process-worker-{1..4} -&amp;gt; Write-worker-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Compression
&lt;/h2&gt;

&lt;p&gt;In my &lt;a href="https://wheynelau.dev/posts/compression-with-ztsd/" rel="noopener noreferrer"&gt;Compression&lt;/a&gt; post, I mentioned that benchmarks should be done to know whether your use case supports compressions. For write once, read many scenarios, higher compression values may help.&lt;/p&gt;

&lt;p&gt;Here is a measurement for an IO-constrained scenario (reading a JSONL file from NAS):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;ZST: 100000it [00:05, 17220.01it/s]  (9.47 MB/s)
Raw: 100000it [00:40, 2492.39it/s]  (11.15 MB/s)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because data is compressed, you can read more data per buffer. More lines are stored per MB of compressed JSONL compared to its raw form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Less is more
&lt;/h2&gt;

&lt;p&gt;Less work means more efficient processing. It's about eliminating wasted work, not always adding a cache everywhere.&lt;/p&gt;

&lt;p&gt;If filtering takes 1s per line and processing takes 5s per line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Process then filter on 10000 lines: &lt;code&gt;10000 * 6s = 60000s&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Filter then process on 10000 lines (50% bad): &lt;code&gt;10000 * 1s + 5000 * 5s = 35000s&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No complex code, no need for compiled languages. Algorithmic complexity matters too. Choosing the right data structure — a set for membership checks instead of a list, a deque instead of a list for queue operations — can eliminate entire classes of wasted work regardless of language.&lt;/p&gt;

&lt;p&gt;The full version with code examples and benchmarks is on &lt;a href="https://wheynelau.dev/posts/2026-03-27-learnings-of-the-poor/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>optimization</category>
      <category>iterators</category>
      <category>programming</category>
    </item>
    <item>
      <title>Benchmarking LLM Inference: TTFT, ITL &amp; Throughput</title>
      <dc:creator>Wayne</dc:creator>
      <pubDate>Sun, 26 Apr 2026 05:05:46 +0000</pubDate>
      <link>https://dev.to/wheynelau/how-to-benchmark-llm-inference-performance-ttft-itl-and-throughput-metrics-416p</link>
      <guid>https://dev.to/wheynelau/how-to-benchmark-llm-inference-performance-ttft-itl-and-throughput-metrics-416p</guid>
      <description>&lt;p&gt;When deploying large language models to production, measuring performance accurately is critical. Whether you're using vLLM, SGLang, TensorRT-LLM, or a custom inference stack, you need to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Throughput&lt;/strong&gt;: How many requests per second can your system handle?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency metrics&lt;/strong&gt;: Time to First Token (TTFT), Inter-Token Latency (ITL), and end-to-end latency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token generation speed&lt;/strong&gt;: Tokens per second under different concurrency levels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tail latency&lt;/strong&gt;: P95 and P99 values that affect user experience&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, I'll walk through the key metrics for benchmarking LLMs and share why I built &lt;a href="https://github.com/wheynelau/llmperf-rs" rel="noopener noreferrer"&gt;llmperf-rs&lt;/a&gt;, a Rust-based benchmarking tool that takes a different approach to measuring them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Existing Tools Fell Short
&lt;/h2&gt;

&lt;p&gt;While working with &lt;a href="https://github.com/ray-project/llmperf" rel="noopener noreferrer"&gt;ray-project/llmperf&lt;/a&gt;, which is now archived, I noticed it calculates Inter-Token Latency (ITL) by averaging per-request first, then aggregating those averages. That works for many use cases, but I needed to preserve individual latency spikes during testing.&lt;/p&gt;

&lt;p&gt;There's also &lt;a href="https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/perf_analyzer/genai-perf/README.html" rel="noopener noreferrer"&gt;genai-perf&lt;/a&gt;, which was very in-depth. My only problem was not being able to run it natively on Ubuntu 22.04 without a Docker container. As of an edit (Apr 2026), they've sunsetted &lt;code&gt;genai-perf&lt;/code&gt; in favor of &lt;a href="https://github.com/ai-dynamo/aiperf" rel="noopener noreferrer"&gt;aiperf&lt;/a&gt;, which I haven't tried but looks comprehensive. &lt;a href="https://docs.vllm.ai/en/latest/benchmarking/cli/#dataset-overview" rel="noopener noreferrer"&gt;vllm-bench&lt;/a&gt; is solid too, but requires installing &lt;code&gt;vllm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The goal was a simple binary that runs almost anywhere with minimal dependencies. It was also a learning project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metrics
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Time To First Token (TTFT)
&lt;/h3&gt;

&lt;p&gt;TTFT measures how quickly the model begins responding after receiving your request. For interactive apps, this is the perceived latency before any output appears. It's also important for RAG-based applications where a large chunk of processing happens at the prefill stage.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TTFT = first_token_timestamp - request_start_timestamp&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Lower is better.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inter-Token Latency (ITL)
&lt;/h3&gt;

&lt;p&gt;ITL is the time between consecutive tokens during generation. Spikes can come from multiple issues, most commonly network problems. ITL is usually consistent due to how KV caches and the computation works.&lt;/p&gt;

&lt;p&gt;When testing against vLLM, I noticed high ITL spikes happen when you benchmark close to the context limit. I suspect this is due to vLLM evicting requests that exceed the KV cache size. If 3 requests come in with &lt;code&gt;0.8x&lt;/code&gt; context length and &lt;code&gt;0.2x&lt;/code&gt; for generation, but the GPU only has room for &lt;code&gt;2.8x&lt;/code&gt;, one request will be preempted. &lt;a href="https://docs.vllm.ai/en/latest/configuration/optimization/#preemption" rel="noopener noreferrer"&gt;vllm preemption docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Aggregation: concatenate ALL ITL values across all responses, then compute statistics. Each response produces &lt;code&gt;N-1&lt;/code&gt; ITL values (where &lt;code&gt;N&lt;/code&gt; is the token count). By aggregating raw values instead of per-request averages, you preserve the true distribution including outliers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Throughput Metrics
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prefill TPS&lt;/strong&gt; counts tokens processed per second during the prefill phase:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Prefill TPS = input_tokens / TTFT&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;However, prefill TPS doesn't accurately reflect system performance, because TTFT includes queue wait time, not just actual processing time. Under load, your request might sit in a queue before prefill starts, so a lower prefill TPS often reflects queue contention, not the system's processing capability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decode TPS&lt;/strong&gt; is tokens generated per second during the decode phase:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Decode TPS = output_tokens / (final_time - decode_start_time)&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Matters Most
&lt;/h2&gt;

&lt;p&gt;For production serving, focus on &lt;strong&gt;TTFT&lt;/strong&gt;, &lt;strong&gt;ITL stats&lt;/strong&gt;, and maybe &lt;strong&gt;RPM&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;TTFT is the perceived responsiveness of your system. ITL statistics reveal decode-phase issues that throughput hides: the 99th percentile and max ITL expose preemption events from KV cache limits and network issues. ITL matters less for batch jobs or non-streaming APIs where users don't watch tokens arrive in real-time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token Counting
&lt;/h2&gt;

&lt;p&gt;Accurate metrics need accurate token counts. llmperf-rs handles this two ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;API response&lt;/strong&gt; gets priority: most OpenAI-compatible endpoints return token counts in the &lt;code&gt;usage&lt;/code&gt; field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokenizer (optional)&lt;/strong&gt;: the default Llama tokenizer ships inside the binary, so no network is needed. For exact input counts, override with a model-specific tokenizer. Note that chat templates can cause &amp;lt;10 token variance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The original llmperf uses a single tokenizer for all models. Different models use different tokenizers, so llmperf-rs lets you specify the correct one. For example, &lt;code&gt;Llama-2&lt;/code&gt; has a vocab size of &lt;code&gt;32000&lt;/code&gt;, while &lt;code&gt;Qwen3-4B&lt;/code&gt; has &lt;code&gt;151936&lt;/code&gt;. In my testing, setting input tokens to &lt;code&gt;8192&lt;/code&gt; against a Qwen endpoint while using the default llama tokenizer returned values around &lt;code&gt;7363-7376&lt;/code&gt; tokens. That's a real error if you care about exact numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validating Your Results
&lt;/h2&gt;

&lt;p&gt;Benchmark runs should ideally end with &lt;code&gt;finish_reason = length&lt;/code&gt;, meaning the model hit the &lt;code&gt;max_tokens&lt;/code&gt; limit.&lt;/p&gt;

&lt;p&gt;An edit (July 2026): with modern models, &lt;code&gt;finish_reason = stop&lt;/code&gt; is common even with a high &lt;code&gt;max_tokens&lt;/code&gt;, because the model may refuse the request or simply end early. Treat stops as acceptable, but be aware they add variance to output tokens, which then adds noise to RPM and E2E latency. I generally check the ratio of &lt;code&gt;length&lt;/code&gt; vs &lt;code&gt;stop&lt;/code&gt; rather than rejecting every &lt;code&gt;stop&lt;/code&gt; outright.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use llmperf-rs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use it when&lt;/strong&gt;: running benchmarks with minimal dependencies (single binary, Docker image), testing OpenAI-compatible endpoints (vLLM, Ollama, local APIs), wanting low overhead (Rust, no Ray/ZMQ), or just needing a quick level-above-&lt;code&gt;curl&lt;/code&gt; way to test an endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider alternatives when&lt;/strong&gt;: you need GPU-level metrics (trtllm-bench or aiperf), vLLM-specific features, extensive reporting dashboards, a Python-first workflow, or distributed testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ITL Matters Even When Throughput Looks Good
&lt;/h2&gt;

&lt;p&gt;High throughput with bad ITL means tokens arrive in bursts, and chat users notice the choppy streaming. ITL spikes (p99 &amp;gt; 100ms) often indicate preemption or network issues. For non-user-facing cases like agentic coding, throughput may matter more than ITL specifics.&lt;/p&gt;

&lt;p&gt;The full version with the detailed metrics documentation, installation steps, and example output is on &lt;a href="https://wheynelau.dev/posts/2025-12-15-benchmarking-performance/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>benchmarking</category>
      <category>rust</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
