<?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: Kristiyan Stoyanov</title>
    <description>The latest articles on DEV Community by Kristiyan Stoyanov (@kstoyanovai).</description>
    <link>https://dev.to/kstoyanovai</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%2F3999391%2F0b4a4fb8-c2b4-4fed-9c31-c14e371ede9c.jpeg</url>
      <title>DEV Community: Kristiyan Stoyanov</title>
      <link>https://dev.to/kstoyanovai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kstoyanovai"/>
    <language>en</language>
    <item>
      <title>How does clean architecture impact token costs and execution times?</title>
      <dc:creator>Kristiyan Stoyanov</dc:creator>
      <pubDate>Thu, 17 Sep 2026 12:10:51 +0000</pubDate>
      <link>https://dev.to/kstoyanovai/how-does-clean-architecture-impact-token-costs-and-execution-times-5g1g</link>
      <guid>https://dev.to/kstoyanovai/how-does-clean-architecture-impact-token-costs-and-execution-times-5g1g</guid>
      <description>&lt;p&gt;I have been wondering whether the economics of software architecture are changing as agents write more of our code. If an agent does most of the implementation, does the structure we put in place still save enough work later to justify its cost?&lt;/p&gt;

&lt;p&gt;As a principal engineer, I started with a fairly strong expectation. Clear boundaries, architectural guidance and tests should help an agent make changes without having to understand everything at once. I expected that investment to become more useful as a project grew.&lt;/p&gt;

&lt;p&gt;So I built an experiment around two versions of the same service. I started with feature additions, tried harder challenges, grew both repositories, and finally tested a change specifically chosen to benefit hexagonal architecture.&lt;/p&gt;

&lt;p&gt;This follows my &lt;a href="https://dev.to/kstoyanovai/i-expected-hexagonal-architecture-to-help-my-coding-agent-it-didnt-3blg"&gt;first post&lt;/a&gt;, but I will go through the setup and the results here so you do not need to read that first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment and the setup
&lt;/h2&gt;

&lt;p&gt;The application is ChargeLedger, a Java service for an EV charging network. It turns charging sessions into invoices, with energy charges, idle charges, tax and stored totals. Over the experiment it gained effective-dated tariffs, refunds, tenant isolation and other billing behavior.&lt;/p&gt;

&lt;p&gt;The rules matter. Old invoices must keep their original prices. Retrying an operation must not refund twice. Financial changes and their audit records must stay consistent.&lt;/p&gt;

&lt;p&gt;I prepared two versions with the same baseline business requirements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Starting approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hexagonal&lt;/td&gt;
&lt;td&gt;Separate domain and application code, ports and adapters, architectural guidance, and architecture tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flat&lt;/td&gt;
&lt;td&gt;The same functional requirements, with no prescribed architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;“Flat” is the experiment's label. I did not ask the model to write bad code or put everything in one file. It made its own choices, including shared pricing and persistence code. Both versions had tests.&lt;/p&gt;

&lt;p&gt;The stack started with Java 21, Javalin, SQLite and Gradle. A local Qwen model ran on my DGX Spark through vLLM. OpenCode ran on my Mac. ChatGPT/Codex helped prepare requirements, build the external evaluator and orchestrate the runs. The local model implemented the measured application changes. The original hexagonal baseline received both local-model and OpenAI assistance during preparation; that preparation was outside the measured time.&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%2Fsmxfbatzp9ooobg3kudq.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%2Fsmxfbatzp9ooobg3kudq.png" alt="Experiment setup: OpenAI ChatGPT and Codex prepare tasks, validation and orchestration. OpenCode on the Mac runs the coding agent and local checks. Qwen is served through vLLM on the NVIDIA DGX Spark. The two repositories run one at a time." width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each task had a written contract and external validation. The agent worked in a fresh session with the current repository. After implementation, the controller ran the project's tests and the external checks. A failure went back to the agent for repair within a fixed budget. Only one participant ran at a time, and each accepted feature got a commit.&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%2Fhuoxfmtdidaya2dne6w0.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%2Fhuoxfmtdidaya2dne6w0.png" alt="The acceptance loop: read the task and current code, implement and test, run project tests and external checks, then accept and commit. Failed checks return feedback for a bounded repair." width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The main measure is &lt;strong&gt;time to acceptance&lt;/strong&gt;: agent work, project checks, external checks and repairs. It excludes initial project preparation and controller preparation before dispatch. Both sides of a task had the same functional contract and budget, but their architecture guidance and internal test suites differed. This compares two complete setups, not architecture as an isolated variable.&lt;/p&gt;

&lt;p&gt;I also recorded input and output tokens, model steps, tool calls and controller-requested repairs. Input includes context sent repeatedly across model steps; it is not unique source code, GPU work or a bill. Output and reasoning are separate recorded fields. A controller repair is a feedback round after validation, not every correction the agent makes while working.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, nine cumulative features: F1–F9
&lt;/h2&gt;

&lt;p&gt;The first sequence began with CSV export and an offline invoice CLI. It then changed idle billing while preserving historical invoices, added issuance auditing and rollback behavior, and introduced effective-dated tariffs, partial refunds and tenant routing.&lt;/p&gt;

&lt;p&gt;Each feature built on the accepted version of the previous one. I expected the architectural separation to help as more rules and entry points were added.&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%2F21284g2q741giq2eidou.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%2F21284g2q741giq2eidou.png" alt="Time to acceptance for each cumulative feature from F1 to F9, comparing flat and hexagonal. Each feature starts from the previously accepted version." width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;F1–F9 total&lt;/th&gt;
&lt;th&gt;Flat&lt;/th&gt;
&lt;th&gt;Hexagonal&lt;/th&gt;
&lt;th&gt;Hexagonal compared with flat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to acceptance&lt;/td&gt;
&lt;td&gt;165.93 min&lt;/td&gt;
&lt;td&gt;228.57 min&lt;/td&gt;
&lt;td&gt;37.8% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input tokens&lt;/td&gt;
&lt;td&gt;31.25 million&lt;/td&gt;
&lt;td&gt;53.40 million&lt;/td&gt;
&lt;td&gt;70.9% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens&lt;/td&gt;
&lt;td&gt;164,177&lt;/td&gt;
&lt;td&gt;217,296&lt;/td&gt;
&lt;td&gt;32.4% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning tokens&lt;/td&gt;
&lt;td&gt;154,599&lt;/td&gt;
&lt;td&gt;236,416&lt;/td&gt;
&lt;td&gt;52.9% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model steps&lt;/td&gt;
&lt;td&gt;426&lt;/td&gt;
&lt;td&gt;612&lt;/td&gt;
&lt;td&gt;43.7% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool calls&lt;/td&gt;
&lt;td&gt;520&lt;/td&gt;
&lt;td&gt;749&lt;/td&gt;
&lt;td&gt;44.0% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controller-requested repairs&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both implementations passed all nine tasks. Hexagonal took longer overall and logged more tokens, steps and tool calls. The original architectural preparation was not included, so these figures describe subsequent work rather than the full investment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then, six harder challenges: C1–C6
&lt;/h2&gt;

&lt;p&gt;My next question was whether the tasks had been demanding enough. I added six challenges:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Challenge&lt;/th&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;C1&lt;/td&gt;
&lt;td&gt;Calculate a quote without persisting an invoice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C2&lt;/td&gt;
&lt;td&gt;Void a refund while preserving its history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C3&lt;/td&gt;
&lt;td&gt;Import charging sessions from NDJSON through the CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C4&lt;/td&gt;
&lt;td&gt;Create and deliver transactional outbox events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C5&lt;/td&gt;
&lt;td&gt;Reconcile daily tenant balances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C6&lt;/td&gt;
&lt;td&gt;Paginate invoices with a tenant-bound keyset cursor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These were independent branches. Each challenge started from its condition's accepted F9 version, not from the previous challenge. They cannot be treated as six more cumulative features.&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%2F29vb9rr4y29s2iq5kzeb.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%2F29vb9rr4y29s2iq5kzeb.png" alt="Time to acceptance for each independent challenge C1–C6. Hexagonal is faster on C3 batch import and C4 transactional outbox; flat is faster on C1, C2, C5 and C6." width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;C1–C6 total&lt;/th&gt;
&lt;th&gt;Flat&lt;/th&gt;
&lt;th&gt;Hexagonal&lt;/th&gt;
&lt;th&gt;Hexagonal compared with flat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to acceptance&lt;/td&gt;
&lt;td&gt;161.55 min&lt;/td&gt;
&lt;td&gt;174.24 min&lt;/td&gt;
&lt;td&gt;7.9% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input tokens&lt;/td&gt;
&lt;td&gt;33.69 million&lt;/td&gt;
&lt;td&gt;51.23 million&lt;/td&gt;
&lt;td&gt;52.1% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens&lt;/td&gt;
&lt;td&gt;120,784&lt;/td&gt;
&lt;td&gt;179,600&lt;/td&gt;
&lt;td&gt;48.7% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning tokens&lt;/td&gt;
&lt;td&gt;180,707&lt;/td&gt;
&lt;td&gt;153,479&lt;/td&gt;
&lt;td&gt;15.1% less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model steps&lt;/td&gt;
&lt;td&gt;357&lt;/td&gt;
&lt;td&gt;546&lt;/td&gt;
&lt;td&gt;52.9% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool calls&lt;/td&gt;
&lt;td&gt;442&lt;/td&gt;
&lt;td&gt;611&lt;/td&gt;
&lt;td&gt;38.2% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controller-requested repairs&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;One additional repair&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both passed all six challenges. Hexagonal was faster on batch import and transactional outbox delivery; flat was faster on the other four. The overall time difference was smaller, and reasoning tokens went in the other direction from input and output.&lt;/p&gt;

&lt;p&gt;That was a more mixed result. It did not establish that hexagonal was catching up as the code grew, because these were different tasks on independent branches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Growing the repositories: S01–S15
&lt;/h2&gt;

&lt;p&gt;A fair criticism of the first results was that the repositories were small. The matched C2 versions had 4,072 production Java lines in flat and 4,939 in hexagonal. A focused service is a relevant setting, but that does not settle an argument about larger systems.&lt;/p&gt;

&lt;p&gt;I continued from those accepted C2 checkpoints and added fifteen changes in sequence. The first five added ordinary resource management across HTTP, CLI, validation and persistence. The later tasks crossed those resources and changed earlier requirements.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stages&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S01–S05&lt;/td&gt;
&lt;td&gt;Customers and contacts, charging sites, chargers, fleet vehicles and versioned billing profiles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S06–S07&lt;/td&gt;
&lt;td&gt;Assign customers to draft invoices and freeze billing details when an invoice is issued&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S08–S10&lt;/td&gt;
&lt;td&gt;Allow draft reassignment with history, transfer chargers between sites, and manage maintenance windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S11–S12&lt;/td&gt;
&lt;td&gt;Import fleet vehicles with per-row results, then add an all-or-nothing import mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S13–S15&lt;/td&gt;
&lt;td&gt;Redact customer data while preserving financial evidence, generate monthly statements, then change how statement addresses are selected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The changes of direction were intentional. A design has to handle the business changing its mind as well as adding functionality that fits the original plan.&lt;/p&gt;

&lt;p&gt;This sequence inherited &lt;strong&gt;F9 plus C2&lt;/strong&gt;, followed by each accepted S task. It did not contain all six earlier challenges.&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%2F993mthsrcv33hfjfxci2.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%2F993mthsrcv33hfjfxci2.png" alt="Minutes to acceptance for S01–S15. Flat takes less time on thirteen tasks. Hexagonal has small wins on S08 and S15." width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;S01–S15 total&lt;/th&gt;
&lt;th&gt;Flat&lt;/th&gt;
&lt;th&gt;Hexagonal&lt;/th&gt;
&lt;th&gt;Hexagonal compared with flat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to acceptance&lt;/td&gt;
&lt;td&gt;298.86 min&lt;/td&gt;
&lt;td&gt;389.45 min&lt;/td&gt;
&lt;td&gt;30.3% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input tokens&lt;/td&gt;
&lt;td&gt;83.04 million&lt;/td&gt;
&lt;td&gt;126.86 million&lt;/td&gt;
&lt;td&gt;52.8% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens&lt;/td&gt;
&lt;td&gt;278,069&lt;/td&gt;
&lt;td&gt;417,686&lt;/td&gt;
&lt;td&gt;50.2% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning tokens&lt;/td&gt;
&lt;td&gt;203,985&lt;/td&gt;
&lt;td&gt;292,081&lt;/td&gt;
&lt;td&gt;43.2% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model steps&lt;/td&gt;
&lt;td&gt;998&lt;/td&gt;
&lt;td&gt;1,219&lt;/td&gt;
&lt;td&gt;22.1% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool calls&lt;/td&gt;
&lt;td&gt;1,027&lt;/td&gt;
&lt;td&gt;1,505&lt;/td&gt;
&lt;td&gt;46.5% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controller-requested repairs&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both implementations passed all fifteen stages. Hexagonal had small time wins on S08 and S15, but took longer overall.&lt;/p&gt;

&lt;p&gt;These points connect tasks of different difficulty with a growing validation suite. They are not a controlled growth curve or evidence of a specific size threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the repositories looked like by then
&lt;/h2&gt;

&lt;p&gt;By S15, these were substantially larger applications than the C2 starting points.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repository measure&lt;/th&gt;
&lt;th&gt;Flat at C2&lt;/th&gt;
&lt;th&gt;Hexagonal at C2&lt;/th&gt;
&lt;th&gt;Flat at S15&lt;/th&gt;
&lt;th&gt;Hexagonal at S15&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Production Java lines&lt;/td&gt;
&lt;td&gt;4,072&lt;/td&gt;
&lt;td&gt;4,939&lt;/td&gt;
&lt;td&gt;11,709&lt;/td&gt;
&lt;td&gt;15,205&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production Java files&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;84&lt;/td&gt;
&lt;td&gt;239&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test Java lines&lt;/td&gt;
&lt;td&gt;4,480&lt;/td&gt;
&lt;td&gt;5,836&lt;/td&gt;
&lt;td&gt;10,305&lt;/td&gt;
&lt;td&gt;13,724&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test Java files&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are physical source lines, including comments and blanks. They exclude the external evaluator, dependencies and generated build output.&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%2Ffbuermsl9kui8xnncj5c.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%2Ffbuermsl9kui8xnncj5c.png" alt="Production repository growth from C2 to S15. Flat grows from 4,072 to 11,709 lines and from 37 to 84 files. Hexagonal grows from 4,939 to 15,205 lines and from 85 to 239 files." width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They were still single services, not large enterprise systems. But they had grown beyond the original few thousand lines and accumulated more behavior. That addressed part of the size criticism without turning line count into a measure of quality or difficulty.&lt;/p&gt;

&lt;p&gt;At this point I was surprised. I had expected hexagonal to start winning more consistently as the application grew and requirements changed. It had individual wins, but the larger cumulative sequence still took 30.3% longer. More structure had not produced the general implementation advantage I expected.&lt;/p&gt;

&lt;p&gt;So I chose a one-off task designed to give the existing boundaries a strong opportunity to help: replace the persistence backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  S16: a change chosen to favor hexagonal
&lt;/h2&gt;

&lt;p&gt;Both implementations had to support PostgreSQL across the service while keeping SQLite working. This directly exercises the replaceable persistence boundary that ports and adapters are supposed to provide.&lt;/p&gt;

&lt;p&gt;It was more than changing a connection string. PostgreSQL had to preserve the accumulated behavior, survive restarts and pass checks involving separate JVMs, concurrent invoice operations, refunds, tenant isolation and invalid configuration. The old SQLite regression suite still had to pass.&lt;/p&gt;

&lt;p&gt;This was &lt;strong&gt;not&lt;/strong&gt; a historical data migration or a zero-downtime rollout. PostgreSQL and the same JDBC dependency were prepared for both projects before timing. Each run had a three-hour budget. Hexagonal ran first, then flat.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;S16 only&lt;/th&gt;
&lt;th&gt;Flat&lt;/th&gt;
&lt;th&gt;Hexagonal&lt;/th&gt;
&lt;th&gt;Hexagonal compared with flat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to acceptance&lt;/td&gt;
&lt;td&gt;71.25 min&lt;/td&gt;
&lt;td&gt;38.92 min&lt;/td&gt;
&lt;td&gt;45.4% less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent work&lt;/td&gt;
&lt;td&gt;63.64 min&lt;/td&gt;
&lt;td&gt;31.10 min&lt;/td&gt;
&lt;td&gt;51.1% less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External evaluation&lt;/td&gt;
&lt;td&gt;7.56 min&lt;/td&gt;
&lt;td&gt;7.77 min&lt;/td&gt;
&lt;td&gt;2.8% more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input tokens&lt;/td&gt;
&lt;td&gt;24.53 million&lt;/td&gt;
&lt;td&gt;14.75 million&lt;/td&gt;
&lt;td&gt;39.9% less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens&lt;/td&gt;
&lt;td&gt;55,523&lt;/td&gt;
&lt;td&gt;32,504&lt;/td&gt;
&lt;td&gt;41.5% less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning tokens&lt;/td&gt;
&lt;td&gt;61,492&lt;/td&gt;
&lt;td&gt;23,011&lt;/td&gt;
&lt;td&gt;62.6% less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model steps&lt;/td&gt;
&lt;td&gt;174&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;31.0% less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool calls&lt;/td&gt;
&lt;td&gt;173&lt;/td&gt;
&lt;td&gt;119&lt;/td&gt;
&lt;td&gt;31.2% less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controller-requested repairs&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both passed their first controller submission. That does not mean the agents made no mistakes or ran no internal test-and-fix loops.&lt;/p&gt;

&lt;p&gt;Here, hexagonal had a clear advantage in time and recorded agent activity. Most of the time difference came from implementation work; external evaluation took roughly the same time on both sides.&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%2F48e6vv3ljug6od6rdub6.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%2F48e6vv3ljug6od6rdub6.png" alt="S16 time breakdown. Agent work takes 63.64 minutes in flat and 31.10 in hexagonal. External evaluation takes 7.56 and 7.77 minutes respectively." width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in the code
&lt;/h2&gt;

&lt;p&gt;The hexagonal implementation added PostgreSQL repositories and supporting connection, schema and transaction code. Existing domain code, application service, ports, HTTP and CLI adapters, and SQLite adapters stayed unchanged. Among existing production files, only configuration and startup wiring changed.&lt;/p&gt;

&lt;p&gt;Flat reused its central repository, introduced database-specific branches and locking behavior, and translated its SQLite schema into PostgreSQL form. Its HTTP and business value classes stayed unchanged too. Both designs accommodated the change, but they did so differently.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Production code in S16&lt;/th&gt;
&lt;th&gt;Flat&lt;/th&gt;
&lt;th&gt;Hexagonal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New files&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing files modified&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines added&lt;/td&gt;
&lt;td&gt;390&lt;/td&gt;
&lt;td&gt;4,527&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines removed&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Net new lines&lt;/td&gt;
&lt;td&gt;307&lt;/td&gt;
&lt;td&gt;4,516&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final production lines&lt;/td&gt;
&lt;td&gt;12,016&lt;/td&gt;
&lt;td&gt;19,721&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final production files&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;250&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsmr4qln99x66076kk1q.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%2Fhsmr4qln99x66076kk1q.png" alt=" " width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Much of the hexagonal PostgreSQL repository code closely follows the SQLite implementations. The separation kept the existing core stable, but left two sets of persistence logic to maintain.&lt;/p&gt;

&lt;p&gt;Flat avoided most of that duplication. It also put more database-specific decisions into an already large repository. Its schema translation uses text substitutions, which makes future schema changes something to review carefully.&lt;/p&gt;

&lt;p&gt;The faster clock and the smaller diff each describe part of the result. Neither settles which implementation will be easier to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results across the phases
&lt;/h2&gt;

&lt;p&gt;The broad pattern was higher accumulated time and input/output tokens for hexagonal in the feature and challenge phases, followed by a reversal on the adapter task.&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%2Fjf22nugyae9ej28sq9yz.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%2Fjf22nugyae9ej28sq9yz.png" alt="Time to acceptance by phase. F1–F9: flat 165.93 minutes, hexagonal 228.57. Independent C1–C6: flat 161.55, hexagonal 174.24. S01–S15: flat 298.86, hexagonal 389.45. S16: flat 71.25, hexagonal 38.92." width="800" height="467"&gt;&lt;/a&gt;&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%2Fnubzzivtsvqqmpx27y9x.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%2Fnubzzivtsvqqmpx27y9x.png" alt="Reported input tokens in millions. F1–F9: flat 31.25M, hexagonal 53.40M. Independent C1–C6: flat 33.69M, hexagonal 51.23M. S01–S15: flat 83.04M, hexagonal 126.86M. S16: flat 24.53M, hexagonal 14.75M." width="800" height="467"&gt;&lt;/a&gt;&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%2Fd9nikvqqu66n3t7273wh.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%2Fd9nikvqqu66n3t7273wh.png" alt="Reported output tokens in thousands. F1–F9: flat 164.18K, hexagonal 217.30K. Independent C1–C6: flat 120.78K, hexagonal 179.60K. S01–S15: flat 278.07K, hexagonal 417.69K. S16: flat 55.52K, hexagonal 32.50K. Reasoning is recorded separately." width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Within the cumulative S extension, the adapter win did not recover all the earlier time difference.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time accounting&lt;/th&gt;
&lt;th&gt;Minutes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Extra hexagonal time across S01–S15&lt;/td&gt;
&lt;td&gt;90.59&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time saved by hexagonal on S16&lt;/td&gt;
&lt;td&gt;32.33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra hexagonal time still remaining&lt;/td&gt;
&lt;td&gt;58.26&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;S16 recovered 35.7% of that gap. Across S01–S16, flat totaled 370.12 minutes and hexagonal 428.37 minutes, or 15.7% more.&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%2Fqxfw1boksvtz4v7frf2j.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%2Fqxfw1boksvtz4v7frf2j.png" alt="S16 recovers 32.33 minutes of the earlier 90.59-minute gap, leaving 58.26 minutes. This recovers 35.7% of the gap. Original preparation time is not included." width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is measured run time, not a complete return-on-investment calculation. Original preparation, future maintenance and human debugging are outside it. One replacement also cannot establish the cost of the next one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the evidence can support
&lt;/h2&gt;

&lt;p&gt;This is one service and one run per condition per task. The starting implementations, guidance and internal tests differ. I alternated which version ran first during S01–S15; S16 had a fixed order. Server-wide prefix-cache counters during S16 showed about 97% hits for both intervals, but those counters can include other clients and cannot establish each run's exact benefit.&lt;/p&gt;

&lt;p&gt;The first S01 attempt also exposed error-response expectations that the written contract had not specified clearly. I clarified the contract and restarted both sides from their original C2 checkpoints. All S01–S15 figures here use that fresh paired rerun. The invalidated attempts are kept separately.&lt;/p&gt;

&lt;p&gt;The results show how these implementations handled these tasks. They do not establish a scale threshold, long-term maintainability or production readiness. Passing the external suite means passing the behavior it covers. This study also did not compare agents with human developers, so it cannot by itself prove that agents changed the economics.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://zenodo.org/records/22806994" rel="noopener noreferrer"&gt;version 2 technical report and supporting data&lt;/a&gt; cover the full experiment described here, including S01–S16, the methods, source checkpoints and limitations.&lt;/p&gt;

&lt;p&gt;AI systems assisted with planning, orchestration, analysis, visuals and drafting. The measured application changes were made by the local coding agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  My opinion: the architectural investment still has value
&lt;/h2&gt;

&lt;p&gt;The following is my interpretation, informed by the results and my experience as a principal engineer.&lt;/p&gt;

&lt;p&gt;The adapter task is a concrete example of architectural investment helping. The existing boundary gave the agent a place to implement PostgreSQL without changing the application core. It reached acceptance with less time, fewer tool calls and fewer tokens. That is useful value, even though it had not recovered the earlier time difference.&lt;/p&gt;

&lt;p&gt;I no longer expect that same investment to make every kind of change cheaper. For the feature work we tested, the structured version usually took longer. The benefit appeared most clearly when the task matched a boundary the architecture had already established.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;But how often do you change your database?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If database replacement is the reason we give for a design, we should ask how often we expect to use that flexibility. Most of the work on a service may be new fields, validation rules, reports and changes to existing behavior. Those changes deserve a place in the calculation too.&lt;/p&gt;

&lt;p&gt;A replacement that becomes cheaper does not automatically justify making frequent work more expensive. In this experiment, the adapter task saved about 32 minutes after the preceding fifteen changes had accumulated about 91 extra minutes. That is a tradeoff I want to discuss explicitly when choosing a design.&lt;/p&gt;

&lt;p&gt;Database replacement is also only one reason to separate persistence. Testing, ownership, dependency isolation and keeping business decisions understandable can matter even if the database never changes. If those are the benefits we need, we should name them and assess whether the design actually delivers them.&lt;/p&gt;

&lt;p&gt;What I am questioning is the habit of accepting the ongoing cost of an architecture without being clear about which of its benefits the project needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project timelines change
&lt;/h2&gt;

&lt;p&gt;There is an equally important problem with planning only for the work in front of us: we are often wrong about how long a project will live.&lt;/p&gt;

&lt;p&gt;The proof of concept becomes production. A temporary integration becomes a permanent dependency. A service meant for one customer ends up serving several. By the time a boundary becomes necessary, introducing it may be much harder.&lt;/p&gt;

&lt;p&gt;That is why I would not turn these results into advice to remove structure and let the agent do whatever it wants. Someone still has to understand the system, review a financial change or investigate a failed refund during an outage.&lt;/p&gt;

&lt;p&gt;My default would be to keep important responsibilities clear and behavior well tested, then add separation where the cost of introducing it later would be high. Pricing rules should have an identifiable home. Transaction ownership should be clear. A second real integration is a stronger reason for an abstraction than a diagram that says every service needs one.&lt;/p&gt;

&lt;p&gt;A larger experiment could test that approach directly: a third implementation with a few deliberate boundaries, followed by both routine changes and disruptive ones. We have not established whether that would offer the best balance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: choose the tradeoff, not the diagram
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Architectural investment can make a major change easier while adding cost to smaller, more frequent changes.&lt;/strong&gt; That is the tradeoff this experiment made concrete for me. The existing persistence boundary helped with PostgreSQL; the same overall setup took longer across the preceding feature work.&lt;/p&gt;

&lt;p&gt;This does not mean that every big change favors hexagonal or that every small change favors flat. Both implementations completed the adapter task, and some earlier tasks also favored hexagonal. The useful question is whether the design fits the changes and risks of this particular service.&lt;/p&gt;

&lt;p&gt;For me, evaluating that case by case means being specific: what are we likely to change, what behavior must remain safe, what does the structure cost during ordinary work, and how difficult would it be to introduce later? We also need to allow for a longer project lifetime than the current plan suggests.&lt;/p&gt;

&lt;p&gt;Agents are making writing and shipping code faster in my own work. I believe that makes it more important to revisit the practices we apply by default. Strong behavioral validation and understandable responsibilities remain worth investing in. Additional layers and abstractions should have a reason tied to the system we are building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I would start with the simplest design that protects those needs, and let further architectural investment earn its place. I would not follow an architecture diagram blindly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I am hyped to keep exploring this. Which boundary in your own service has earned its cost, and what change made that clear?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>programming</category>
      <category>testing</category>
    </item>
    <item>
      <title>I expected hexagonal architecture to help my coding agent. It didn't.</title>
      <dc:creator>Kristiyan Stoyanov</dc:creator>
      <pubDate>Tue, 15 Sep 2026 20:39:41 +0000</pubDate>
      <link>https://dev.to/kstoyanovai/i-expected-hexagonal-architecture-to-help-my-coding-agent-it-didnt-3blg</link>
      <guid>https://dev.to/kstoyanovai/i-expected-hexagonal-architecture-to-help-my-coding-agent-it-didnt-3blg</guid>
      <description>&lt;p&gt;I am a principal engineer, and I started this experiment trying to defend something I have believed for years: invest in a good structure now, and later changes should get easier.&lt;/p&gt;

&lt;p&gt;Two or three years ago, I would have pushed back on the claim that hexagonal architecture was mostly overhead. I wanted to show that the investment would also help a coding agent.&lt;/p&gt;

&lt;p&gt;Across nine features, the hexagonal version took &lt;strong&gt;38% longer to reach acceptance&lt;/strong&gt;. That was not the result I had hoped for.&lt;/p&gt;

&lt;p&gt;I still care about clean code. But this experiment made me much less willing to prescribe an architecture before I can explain what it buys us.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question behind the experiment
&lt;/h2&gt;

&lt;p&gt;In my work, agents are changing how quickly we can write and ship code. I think that changes which engineering practices deserve our time. This experiment did not compare agents with humans; it tested a question about the code we ask agents to work in.&lt;/p&gt;

&lt;p&gt;I am less interested in whether a method has crossed a line-count threshold than in whether I can find the pricing rule, trace a state change, and understand where a transaction begins and ends. I still want good names. I still want business rules to have a clear home. I still want tests that exercise the behavior that matters.&lt;/p&gt;

&lt;p&gt;What I am questioning is indirection added by habit: the interface with one implementation and no clear boundary, the pass-through method, or the extraction that scatters one operation across several files without making it easier to understand or change. Every layer has a cost for the person and the agent that must read it. I now think that cost needs a concrete justification.&lt;/p&gt;

&lt;p&gt;ChargeLedger gave me a way to test that belief. It is a Java service for an EV charging network. It turns a completed session into an invoice: energy used, time spent plugged in after charging completes, tax, and a total that must remain stable once stored. It is small, but its requirements were not trivial. A tariff change must not rewrite an old invoice. A retry must not refund twice. If an audit write fails, issuing the invoice must roll back with it.&lt;/p&gt;

&lt;p&gt;Those are the kinds of guarantees I want software structure to make easier to preserve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two starting points, one acceptance loop
&lt;/h2&gt;

&lt;p&gt;I prepared two implementations with the same baseline business contract, Java 21, Javalin, SQLite, Gradle, and dependency pins.&lt;/p&gt;

&lt;p&gt;The hexagonal condition was asked to use a domain isolated from HTTP and SQLite, application use cases, ports, adapters, edge composition, and architecture tests. The flat condition received the same functionality requirements but an architecture-neutral prompt. “Flat” does not mean deliberately messy. It became a direct, coherent application with shared pricing, persistence, transactions, HTTP, and CLI behavior.&lt;/p&gt;

&lt;p&gt;I compared two complete setups. Their starting code, architecture guidance and test suites differed. I used both the local model and OpenAI assistance to prepare the hexagonal baseline; the flat baseline was generated by the local model. I excluded that preparation time from the measured clock. The results therefore cannot tell us that hexagonal architecture alone caused anything, or whether either approach has a better total lifecycle cost.&lt;/p&gt;

&lt;p&gt;I served the local model on my DGX Spark and ran OpenCode on my Mac. ChatGPT/Codex helped prepare the tasks, build the evaluator and orchestrate the runs. Only one participant job ran at a time, so the two projects were not competing for the model.&lt;/p&gt;

&lt;p&gt;For every measured task, the agent got a frozen public contract in a fresh session. The controller then ran project tests and a separate public-interface evaluator. If a product-level check failed, it returned feedback for a bounded repair attempt. A run counted as accepted only when both gates passed.&lt;/p&gt;

&lt;p&gt;The timing is &lt;strong&gt;time to acceptance&lt;/strong&gt;: agent work, project tests, external checks, and any repair. The clock stopped only after validation passed.&lt;/p&gt;

&lt;p&gt;The baseline suite had 14 original tests. Across the study, all 30 measured runs passed their project and external acceptance checks. That means the implementations met the covered behavior. The suite can still miss things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result I did not expect
&lt;/h2&gt;

&lt;p&gt;I gave both implementations nine cumulative feature tasks. They began with CSV export and an offline invoice CLI, then changed idle billing while keeping historic invoices fixed. They added issuance audit events and atomic rollback behavior, then effective-dated tariffs, partial refunds with concurrent balance protection, and tenant routing across the service.&lt;/p&gt;

&lt;p&gt;I expected the architectural investment to become useful as that surface area grew.&lt;/p&gt;

&lt;p&gt;Instead, the flat trajectory reached acceptance in &lt;strong&gt;165.93 minutes&lt;/strong&gt;. The hexagonal trajectory took &lt;strong&gt;228.57 minutes&lt;/strong&gt;: &lt;strong&gt;37.8% longer&lt;/strong&gt;. Upfront preparation was outside both clocks.&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%2Ft67smecjwz7i28e9aipf.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%2Ft67smecjwz7i28e9aipf.png" alt="Time to accepted F1–F9 features: flat 165.93 minutes, hexagonal 228.57 minutes" width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The time budget was 30 minutes per task for F1–F6 and 90 minutes for F7–F9, with the same budget for each side of a pair. Each task occurred once per condition, so those lines are not a statistical trend. They connect outcomes from a cumulative sequence whose task difficulty and validation scope changed over time. But they are still the result that made me pause. There were individual wins for hexagonal, but no overall speed advantage.&lt;/p&gt;

&lt;p&gt;The trace data did not offer a simple explanation. Across F1–F9, the flat runs logged 31.25 million input tokens and the hexagonal runs 53.40 million. Those totals include context repeatedly supplied across model steps. They are not billed dollars, GPU work, or unique repository text. Hexagonal also logged more model steps and tool calls. Those are observations about this setup, not a verdict on an architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  I tried harder work
&lt;/h2&gt;

&lt;p&gt;My first response was that the feature sequence might have been too easy. So I added six harder contracts: calculate a quote without creating a row, void a refund while preserving history, import NDJSON offline, create and deliver transactional outbox events, reconcile daily tenant balances, and paginate invoices with a tenant-bound keyset cursor.&lt;/p&gt;

&lt;p&gt;These were not F10 through F15. Each challenge started independently from its condition’s accepted F9 checkpoint. None inherited the previous challenge’s implementation.&lt;/p&gt;

&lt;p&gt;Across the six challenge pairs, flat took &lt;strong&gt;161.55 minutes&lt;/strong&gt; and hexagonal took &lt;strong&gt;174.24 minutes&lt;/strong&gt;, about &lt;strong&gt;8% longer&lt;/strong&gt; for hexagonal. Hexagonal was faster on two tasks: batch import and transactional outbox delivery. Flat was faster on the other four.&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%2F7x4cxxmbiaxb7knb71zx.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%2F7x4cxxmbiaxb7knb71zx.png" alt="Six independent F9 challenges: hexagonal is faster on batch import and outbox delivery; flat is faster on the other four" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is a more mixed result. I cannot treat the smaller gap as evidence that hexagonal would eventually overtake flat: these were different tasks on separate branches, with one run per condition.&lt;/p&gt;

&lt;p&gt;The input-token totals were also higher in the structured condition: 33.69 million flat and 51.23 million hexagonal across the challenges. Again, repeated context is part of those values. They are evidence about recorded agent activity, not a price tag or a quality metric.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the accepted code looked like
&lt;/h2&gt;

&lt;p&gt;I then looked at the largest matched challenge versions: C2, refund voiding.&lt;/p&gt;

&lt;p&gt;The flat version had 4,072 production Java lines in 37 files. The hexagonal version had 4,939 lines in 85 files. Flat’s largest production file accounted for about 36% of its production lines; hexagonal’s largest was about 18%.&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%2Fij6983handjczg58k114.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%2Fij6983handjczg58k114.png" alt="Accepted C2 code shape: lines, files, and largest-file concentration" width="800" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Flat concentrated more code in one place; hexagonal spread it across more files. That gives us something concrete to inspect, but a file count cannot tell us which system will be easier to maintain.&lt;/p&gt;

&lt;p&gt;The code review added some context. The flat code put shared pricing in &lt;code&gt;Pricing&lt;/code&gt; and issuance in &lt;code&gt;SessionRepository.issue&lt;/code&gt;. Hexagonal used &lt;code&gt;PricingPolicy&lt;/code&gt;, &lt;code&gt;LedgerService&lt;/code&gt; and ports. At F9, more persistence and migration work sat in the flat repository, while hexagonal separated schema migration and tenant wiring across roles. Both had shared business operations and transaction handling. Neither was a caricature.&lt;/p&gt;

&lt;p&gt;The tradeoff is real. The direct path can be easier to trace. The separated path can make dependency direction and role boundaries explicit. I no longer think I should assume either benefit wins before I can name the problem it solves.&lt;/p&gt;

&lt;p&gt;These are small applications. That limits the conclusion, but focused services are a relevant setting too. A microservice does not need to become a large codebase before we can ask whether its structure is helping. What we have here is a few thousand production lines with some demanding rules, not evidence about a whole enterprise system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I think changes in the agentic age
&lt;/h2&gt;

&lt;p&gt;My conclusion is an opinion informed by the experiment, not a rule established by it.&lt;/p&gt;

&lt;p&gt;Start simple. Add a boundary when it isolates a dependency, separates a real responsibility, protects a volatile integration, or makes an ownership decision clearer. Do not add it because every service is supposed to have another layer.&lt;/p&gt;

&lt;p&gt;At the same time, simplicity cannot mean “throw it all in one file and trust the agent.” I want one place to change the tariff-selection rule. I want an idempotency key to have a clearly understood scope. I want invoice issuance and auditing to have a traceable all-or-nothing transaction. I want names that tell the next reader where those decisions live.&lt;/p&gt;

&lt;p&gt;And I want stronger behavioral evidence. When code is cheaper to generate, a common external acceptance suite, regression checks, and explicit failure cases become more valuable. Tests only establish what they cover, but that is still much better than treating a confident agent response as proof.&lt;/p&gt;

&lt;p&gt;This also changes how I think about reviews. A nitpick about a line break or another mechanical convention is often a good candidate for automation. A blocking review comment should explain the concrete consequence it prevents: duplicated state, an unsafe retry, an unbounded query, a broken tenant boundary, or a transaction that can partially commit.&lt;/p&gt;

&lt;p&gt;There is another possibility: larger context windows and better retrieval may help agents make use of modular structures more effectively, not just direct code. My hypothesis is that stronger context may expand the range where a simple implementation works well, but this experiment did not test that boundary.&lt;/p&gt;

&lt;p&gt;Proofs of concept also become production more often than we plan. If someone has to investigate an incorrect invoice during a SEV1, they need to find the decision and understand the state change. The benchmark clock stops at acceptance; ownership does not. I want to spend engineering effort where I can explain the behavior it protects or the change it makes easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits are part of the result
&lt;/h2&gt;

&lt;p&gt;This was one model/harness setup, one service, and 15 paired tasks, each run once per condition. The local service was labeled Qwen3.8FlashNext for the project, but frozen records identify the served alias as &lt;code&gt;qwen&lt;/code&gt;; the exact checkpoint and server settings were not independently verified. The evaluator changed through documented amendments, baseline preparation was excluded, and the treatments differed in more than architecture.&lt;/p&gt;

&lt;p&gt;The result changed my mind about a default I used to defend. It leaves plenty of questions open.&lt;/p&gt;

&lt;p&gt;The full method, frozen contracts, acceptance data, figures, and limitations are in the technical report:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://zenodo.org/records/22775539" rel="noopener noreferrer"&gt;Read the technical report and download the data&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Stoyanov, K. (2026). &lt;em&gt;Hexagonal architecture and coding agents: an exploratory study of change acceptance in ChargeLedger&lt;/em&gt; (Version 1.0). Zenodo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI systems assisted with planning, orchestration, analysis, figures, and drafting; the report’s method section describes the coding-agent participant and the limits of the evidence.&lt;/p&gt;

&lt;p&gt;I am hyped to share this and hear from people working through the same questions. In particular: &lt;strong&gt;what layer, interface, or clean-code rule has earned its cost on your team—and what evidence convinced you?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>programming</category>
      <category>testing</category>
    </item>
    <item>
      <title>How I Trained a DFlash Drafter for Speculative Decoding</title>
      <dc:creator>Kristiyan Stoyanov</dc:creator>
      <pubDate>Mon, 07 Sep 2026 16:17:10 +0000</pubDate>
      <link>https://dev.to/kstoyanovai/how-i-trained-a-dflash-drafter-for-speculative-decoding-2pm</link>
      <guid>https://dev.to/kstoyanovai/how-i-trained-a-dflash-drafter-for-speculative-decoding-2pm</guid>
      <description>&lt;h1&gt;
  
  
  If you prefer video format
&lt;/h1&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/HKQ9NTwcdkM" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h1&gt;
  
  
  How I Trained a DFlash Drafter for Speculative Decoding
&lt;/h1&gt;

&lt;p&gt;Running a capable local LLM is often easy. Making it responsive enough for interactive use, coding, or agent workflows is much harder.&lt;/p&gt;

&lt;p&gt;I wanted to improve the decode throughput of a Qwen3.8 27B target model running in my local setup. Rather than changing the target model, I trained a small DFlash draft model and used speculative decoding. This post focuses on the practical workflow: how to prepare training data, choose an online or offline pipeline, train and inspect a drafter, export it, serve it, and benchmark it honestly.&lt;/p&gt;

&lt;p&gt;This is not a universal copy-paste recipe. Exact package names, flags, configurations, and supported architectures move quickly. Treat the commands as a concrete template, pin the versions that work in your environment, and verify the current documentation for your chosen framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are building
&lt;/h2&gt;

&lt;p&gt;A speculative decoding system uses two models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A target model. This is the model whose output behavior you want to preserve.&lt;/li&gt;
&lt;li&gt;A draft model, or drafter. This smaller model proposes several next tokens quickly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At generation time, the drafter proposes a block of tokens. The target model verifies that proposal. It accepts the matching prefix and corrects the first disagreement. Because the target can verify multiple proposed tokens together, speculative decoding can improve output throughput without changing the target model's output distribution.&lt;/p&gt;

&lt;p&gt;The important point is that a drafter is not a general-purpose replacement for the target model. It is a specialized accelerator coupled to a particular target model and serving implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DFlash is interesting
&lt;/h2&gt;

&lt;p&gt;Many speculative approaches draft autoregressively: token one, then token two, then token three. A DFlash drafter uses block diffusion to predict a block of future tokens in one parallel forward pass.&lt;/p&gt;

&lt;p&gt;A simplified view looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt
  |
  +--&amp;gt; Target model produces context features
  |
  +--&amp;gt; DFlash drafter proposes a token block in parallel
  |
  +--&amp;gt; Target model verifies the proposed block
  |
  +--&amp;gt; Accept matching prefix and continue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DFlash uses the target model's internal context features to help the small draft model make better proposals. In particular, its design fuses hidden states from the target and injects that context into the drafter's attention keys and values. This lets the drafter spend its capacity on predicting the next block rather than reconstructing the entire context from scratch.&lt;/p&gt;

&lt;p&gt;This is why two metrics must always be considered together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Acceptance. How many consecutive draft tokens the target accepts.&lt;/li&gt;
&lt;li&gt;Draft speed. How quickly the drafter can create proposed blocks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A slow but accurate drafter may not help. A fast drafter with poor acceptance may add overhead. The metric that matters at the end is measured end-to-end output throughput under a realistic workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites and planning
&lt;/h2&gt;

&lt;p&gt;Before starting a run, confirm these items.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A compatible target model
&lt;/h3&gt;

&lt;p&gt;Your target must be supported by the training code and the serving runtime. The draft model is architecture-specific in practice: tokenizer vocabulary, hidden sizes, RoPE behavior, attention layout, and target hidden-state interfaces all matter.&lt;/p&gt;

&lt;p&gt;Do not assume that a drafter trained for one Qwen version can be paired with another. Pair the exact target checkpoint named by the draft model or training recipe.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A suitable GPU environment
&lt;/h3&gt;

&lt;p&gt;Drafter training can be memory-bandwidth and storage intensive. You will commonly have all of these on disk at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The target model&lt;/li&gt;
&lt;li&gt;Source prompt data&lt;/li&gt;
&lt;li&gt;Regenerated or distilled responses&lt;/li&gt;
&lt;li&gt;Training checkpoints&lt;/li&gt;
&lt;li&gt;Exported Hugging Face artifacts&lt;/li&gt;
&lt;li&gt;Logs and temporary caches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Provision more storage than your first estimate. For large models, 1 TB is a safer starting point than trying to expand a rented instance halfway through a run.&lt;/p&gt;

&lt;p&gt;Use a persistent remote session for every long operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux new &lt;span class="nt"&gt;-s&lt;/span&gt; dflash-train

&lt;span class="c"&gt;# Detach with Ctrl-b then d&lt;/span&gt;
&lt;span class="c"&gt;# Reattach later&lt;/span&gt;
tmux attach &lt;span class="nt"&gt;-t&lt;/span&gt; dflash-train
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back up progress outside the rented machine at regular intervals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rsync &lt;span class="nt"&gt;-avP&lt;/span&gt; /workspace/dflash-runs/ user@your-local-host:/data/dflash-backups/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. A reproducible software stack
&lt;/h3&gt;

&lt;p&gt;Do not blindly install the latest version of every library. The intersection of PyTorch, CUDA, Transformers, FlashAttention, vLLM, SGLang, and a speculative-decoding training framework can be fragile.&lt;/p&gt;

&lt;p&gt;Start with an isolated environment and record the working versions immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip

python &lt;span class="nt"&gt;--version&lt;/span&gt;
pip freeze | &lt;span class="nb"&gt;tee &lt;/span&gt;requirements.lock.txt
nvidia-smi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the project provides a Docker image, development container, or pinned lockfile, prefer it. A known-good container can save days of dependency debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The training pipeline
&lt;/h2&gt;

&lt;p&gt;At a high level, the workflow has five stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Collect prompts
2. Generate target-model responses
3. Train the drafter
4. Export the checkpoint
5. Serve and benchmark
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The training data should resemble the work you actually ask the target model to do. A coding assistant benefits from coding prompts. An agent server benefits from tool calls, structured output, planning traces, and the system prompts it sees in production.&lt;/p&gt;

&lt;p&gt;A generic instruction dataset can produce a usable drafter, but task-aligned prompts are more likely to improve acceptance on your own workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Prepare prompt data
&lt;/h2&gt;

&lt;p&gt;A simple starting format is JSONL: one JSON object per line.&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Write a Python function that retries an HTTP request with exponential backoff."&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Explain the difference between Kubernetes Deployment and StatefulSet."&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For chat models, preserve the structure that the target model expects. Do not flatten messages into ad hoc strings unless you also control and validate the exact chat template.&lt;/p&gt;

&lt;p&gt;Here is a small Python script that samples a larger JSONL dataset reproducibly:&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;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompts-all.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;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompts-sample-30k.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;sample_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30_000&lt;/span&gt;
&lt;span class="n"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;

&lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;selected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;sample_size&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;destination&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;w&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;selected&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;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="c1"&gt;# Fail early if the input is malformed.
&lt;/span&gt;        &lt;span class="n"&gt;f&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="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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wrote &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; examples to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="si"&gt;}&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;Before spending GPU money, inspect examples manually. Look for malformed conversations, duplicate prompts, giant contexts, private data, template artifacts, and prompts that do not represent your intended workload.&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;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 3 prompts-sample-30k.jsonl | jq &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; prompts-sample-30k.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Use the target model to create labels
&lt;/h3&gt;

&lt;p&gt;The drafter should learn the behavior of the target model, not the behavior of whatever dataset originally supplied an answer. Send each prompt to the target model and save the target's generated response. This is the distillation stage.&lt;/p&gt;

&lt;p&gt;First launch an OpenAI-compatible target server. This SGLang command is illustrative; use the version and flags validated for your target model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; sglang.launch_server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model-path&lt;/span&gt; Qwen/Qwen3.8-27B &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 30000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tp-size&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dtype&lt;/span&gt; bfloat16 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mem-fraction-static&lt;/span&gt; 0.80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run the framework's data-regeneration script in a second terminal. The command below shows the inputs you generally need: source prompts, output path, target endpoint, concurrency, generation settings, and resumability.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/regenerate_train_data.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--input&lt;/span&gt; prompts-sample-30k.jsonl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; distilled-qwen3.8-27b.jsonl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--base-url&lt;/span&gt; http://127.0.0.1:30000/v1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; Qwen/Qwen3.8-27B &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--temperature&lt;/span&gt; 0.7 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-tokens&lt;/span&gt; 2048 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resume&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important practical feature here is &lt;code&gt;--resume&lt;/code&gt; or its equivalent. Distillation can run for many hours. Spot instances can disappear, servers can crash, and local networks can drop. A resumable output turns a disaster into an interruption.&lt;/p&gt;

&lt;p&gt;If your target has a supported built-in speculative mode, enabling it during data generation may reduce the time needed to build the distilled dataset. Validate that doing so preserves the target behavior you want to imitate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Choose offline or online training
&lt;/h2&gt;

&lt;p&gt;There are two broad ways to feed target-model information into a drafter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Offline training
&lt;/h3&gt;

&lt;p&gt;Offline training separates the work into two phases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Distilled data
  -&amp;gt; Run target model and save hidden states
  -&amp;gt; Train drafter from saved hidden states
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/prepare_hidden_states.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; Qwen/Qwen3.8-27B &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--input&lt;/span&gt; distilled-qwen3.8-27b.jsonl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-dir&lt;/span&gt; hidden-states-qwen3.8-27b

torchrun &lt;span class="nt"&gt;--nproc_per_node&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8 train_dflash.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config&lt;/span&gt; configs/qwen3.8-27b-dflash.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-path&lt;/span&gt; distilled-qwen3.8-27b.jsonl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hidden-state-path&lt;/span&gt; hidden-states-qwen3.8-27b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easier mental model: prepare features, then train.&lt;/li&gt;
&lt;li&gt;Target and drafter do not have to share live runtime memory during the training step.&lt;/li&gt;
&lt;li&gt;Easier to retry only one stage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disadvantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hidden states can be very large.&lt;/li&gt;
&lt;li&gt;You pay for a separate extraction pass.&lt;/li&gt;
&lt;li&gt;Data preparation can become the bottleneck.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Online training
&lt;/h3&gt;

&lt;p&gt;Online training keeps the target and drafter live together. The training process obtains target features as it trains, often through a shared KV-cache or shared-memory mechanism.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Distilled data
  -&amp;gt; Live target model
  -&amp;gt; Shared context features or KV cache
  -&amp;gt; Live drafter training
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A generic layout looks like this:&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;# Terminal 1: start the target model with the shared-cache integration.&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; sglang.launch_server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model-path&lt;/span&gt; Qwen/Qwen3.8-27B &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 30000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-shared-kv-cache&lt;/span&gt;

&lt;span class="c"&gt;# Terminal 2: run training.&lt;/span&gt;
torchrun &lt;span class="nt"&gt;--nproc_per_node&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8 train_dflash.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config&lt;/span&gt; configs/qwen3.8-27b-dflash-online.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-path&lt;/span&gt; distilled-qwen3.8-27b.jsonl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-base-url&lt;/span&gt; http://127.0.0.1:30000/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoids a separate hidden-state extraction workflow.&lt;/li&gt;
&lt;li&gt;Can use a large GPU more efficiently.&lt;/li&gt;
&lt;li&gt;Faster iteration when the environment is already working.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disadvantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More moving parts.&lt;/li&gt;
&lt;li&gt;Greater runtime memory demand.&lt;/li&gt;
&lt;li&gt;More sensitive to version compatibility between the training process, serving runtime, and shared cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My practical rule is simple: choose offline when you want the least complicated path or have limited runtime memory. Choose online when you have enough GPU headroom and are willing to spend time validating the shared-cache setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Configure DFlash training
&lt;/h2&gt;

&lt;p&gt;A DFlash configuration usually controls the drafter architecture, block size, target feature layers, optimizer settings, dataset handling, and checkpoint cadence.&lt;/p&gt;

&lt;p&gt;Here is an illustrative YAML configuration. Field names vary by framework, so use it as a map of the decisions you need to make.&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;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;target_model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Qwen/Qwen3.8-27B&lt;/span&gt;
  &lt;span class="na"&gt;drafter_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dflash&lt;/span&gt;
  &lt;span class="na"&gt;num_hidden_layers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;hidden_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2048&lt;/span&gt;
  &lt;span class="na"&gt;num_attention_heads&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;
  &lt;span class="na"&gt;num_key_value_heads&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;

&lt;span class="na"&gt;training&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;output_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runs/qwen3.8-27b-dflash&lt;/span&gt;
  &lt;span class="na"&gt;max_steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20000&lt;/span&gt;
  &lt;span class="na"&gt;per_device_train_batch_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;gradient_accumulation_steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;
  &lt;span class="na"&gt;learning_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.0001&lt;/span&gt;
  &lt;span class="na"&gt;warmup_ratio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.03&lt;/span&gt;
  &lt;span class="na"&gt;bf16&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;gradient_checkpointing&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;save_steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
  &lt;span class="na"&gt;logging_steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;

&lt;span class="na"&gt;sequence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;max_length&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4096&lt;/span&gt;
  &lt;span class="na"&gt;block_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;
  &lt;span class="na"&gt;num_anchors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;256&lt;/span&gt;

&lt;span class="na"&gt;loss&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;self_logit_distillation&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;loss_decay_factor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7.0&lt;/span&gt;
  &lt;span class="na"&gt;answer_only_loss&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;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;train_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;distilled-qwen3.8-27b.jsonl&lt;/span&gt;
  &lt;span class="na"&gt;chat_template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;templates/qwen3-train.jinja&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The parameters that deserve attention
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Block size
&lt;/h4&gt;

&lt;p&gt;Block size is the number of token positions in a draft block. Larger blocks create more opportunity for acceleration, but later tokens are generally harder to predict and may be rejected.&lt;/p&gt;

&lt;p&gt;Start with the block size recommended by the implementation and the corresponding reference checkpoint. Do not change it casually at serving time. The drafter is trained around a particular block structure.&lt;/p&gt;

&lt;h4&gt;
  
  
  Drafter depth and size
&lt;/h4&gt;

&lt;p&gt;A larger drafter may improve acceptance, but it consumes more memory and can reduce draft speed. DFlash is attractive because it can predict a block in parallel, but that does not eliminate the cost of a deeper network.&lt;/p&gt;

&lt;p&gt;Start with an upstream architecture configuration that is known to export and serve correctly. Only tune drafter capacity after you have a benchmark harness.&lt;/p&gt;

&lt;h4&gt;
  
  
  Number of steps
&lt;/h4&gt;

&lt;p&gt;A small experimental run is useful for validating the pipeline, but it is not a production training schedule. In my experiment, I used a short staged run: an initial phase, then a lower learning rate phase. That was enough to prove the full path worked, but not necessarily enough to maximize generalization.&lt;/p&gt;

&lt;p&gt;Save checkpoints often enough that a failed rental instance does not waste a day of work:&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;training&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;save_steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
  &lt;span class="na"&gt;save_total_limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Assistant-only loss and chat templates
&lt;/h4&gt;

&lt;p&gt;If you only want to train on assistant responses, ensure that the data pipeline can identify those tokens accurately. Some frameworks require chat templates with explicit generation markers in order to produce an assistant token mask.&lt;/p&gt;

&lt;p&gt;Do not assume a model's default template has the required markers. Test tokenization before beginning a large run.&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;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;

&lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen3.8-27B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;messages&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;role&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;user&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;content&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;Explain a binary search.&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;role&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;assistant&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;content&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;Binary search repeatedly halves a sorted search space.&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="n"&gt;encoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_chat_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tokenize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;return_dict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;return_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;return_assistant_tokens_mask&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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;encoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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;encoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant_masks&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;If no assistant mask is created, fix the template before training. Otherwise, you may waste substantial compute learning to predict user messages, system prompts, or formatting tokens that do not improve generation behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Launch and monitor training
&lt;/h2&gt;

&lt;p&gt;A typical multi-GPU launch uses &lt;code&gt;torchrun&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;torchrun &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--standalone&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--nproc_per_node&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8 &lt;span class="se"&gt;\&lt;/span&gt;
  train_dflash.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config&lt;/span&gt; configs/qwen3.8-27b-dflash-online.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For distributed jobs, log the exact command, config, git commit, container tag, and hardware details alongside every run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rev-parse HEAD | &lt;span class="nb"&gt;tee &lt;/span&gt;runs/qwen3.8-27b-dflash/git-commit.txt
pip freeze | &lt;span class="nb"&gt;tee &lt;/span&gt;runs/qwen3.8-27b-dflash/requirements.txt
nvidia-smi &lt;span class="nt"&gt;-q&lt;/span&gt; | &lt;span class="nb"&gt;tee &lt;/span&gt;runs/qwen3.8-27b-dflash/nvidia-smi.txt
&lt;span class="nb"&gt;cp &lt;/span&gt;configs/qwen3.8-27b-dflash-online.yaml runs/qwen3.8-27b-dflash/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What to monitor
&lt;/h3&gt;

&lt;p&gt;At minimum, watch these signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Training loss should trend downward without exploding or becoming NaN.&lt;/li&gt;
&lt;li&gt;Draft token accuracy should generally rise.&lt;/li&gt;
&lt;li&gt;GPU utilization should be high enough to justify the rental.&lt;/li&gt;
&lt;li&gt;Memory usage should leave a small safety margin.&lt;/li&gt;
&lt;li&gt;Checkpoints should be written successfully.&lt;/li&gt;
&lt;li&gt;Data loading should not starve the GPU.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Accuracy in a training log is useful, but it is not the final serving metric. A per-token training accuracy number does not directly tell you how many consecutive tokens will be accepted in a live speculative loop.&lt;/p&gt;

&lt;p&gt;Also watch for overfitting. An implausibly perfect score on a narrow dataset can mean the drafter memorized a distribution that does not match your real prompts.&lt;/p&gt;

&lt;p&gt;A simple live log filter can help during debugging:&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;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; runs/qwen3.8-27b-dflash/trainer.log | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;--line-buffered&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"step|loss|acc|accuracy|grad_norm|nan"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Export to Hugging Face format
&lt;/h2&gt;

&lt;p&gt;Training checkpoints are often framework-specific. Serving runtimes usually need a Hugging Face compatible export containing at least a &lt;code&gt;config.json&lt;/code&gt; and one or more &lt;code&gt;model.safetensors&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;A typical export command looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/export_hf_checkpoint.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model_path&lt;/span&gt; runs/qwen3.8-27b-dflash/checkpoint-20000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--export_path&lt;/span&gt; exports/qwen3.8-27b-dflash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect the exported directory before attempting to serve it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find exports/qwen3.8-27b-dflash &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 2 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-printf&lt;/span&gt; &lt;span class="s2"&gt;"%f&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sort
ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; exports/qwen3.8-27b-dflash
jq &lt;span class="nb"&gt;.&lt;/span&gt; exports/qwen3.8-27b-dflash/config.json | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important configuration fields vary by implementation, but generally include architecture metadata, vocabulary size, block size, attention dimensions, and the target pairing assumptions.&lt;/p&gt;

&lt;p&gt;Do a cheap load test before copying a large checkpoint to another machine:&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;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoModel&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModel&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exports/qwen3.8-27b-dflash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;trust_remote_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;device_map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Serve the target and drafter
&lt;/h2&gt;

&lt;p&gt;Serving integration changes quickly. Always begin with the current documentation for your runtime and DFlash implementation.&lt;/p&gt;

&lt;p&gt;An example vLLM-style configuration commonly looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm serve Qwen/Qwen3.8-27B &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tensor-parallel-size&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.90 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-config&lt;/span&gt; &lt;span class="s1"&gt;'{
    "method": "dflash",
    "model": "/models/qwen3.8-27b-dflash",
    "num_speculative_tokens": 15
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An SGLang-style deployment can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; sglang.launch_server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model-path&lt;/span&gt; Qwen/Qwen3.8-27B &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-algorithm&lt;/span&gt; DFLASH &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-draft-model-path&lt;/span&gt; /models/qwen3.8-27b-dflash &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tp-size&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dtype&lt;/span&gt; bfloat16 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--trust-remote-code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The draft block is often represented as an anchor token plus masked positions internally. A runtime flag named &lt;code&gt;num_speculative_tokens&lt;/code&gt; may count only proposed tokens, while the training &lt;code&gt;block_size&lt;/code&gt; can include the anchor. Check the implementation's terminology and use the reference serving command where possible.&lt;/p&gt;

&lt;p&gt;Test correctness before measuring speed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://127.0.0.1:8000/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "Qwen/Qwen3.8-27B",
    "messages": [{"role": "user", "content": "Give me three concise Linux troubleshooting tips."}],
    "temperature": 0,
    "max_tokens": 128
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for failed remote-code imports, incompatible model configs, tokenizer mismatches, cache-layout errors, and unsupported attention backends. Solve those before chasing benchmark numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark the right way
&lt;/h2&gt;

&lt;p&gt;Do not benchmark one prompt once and call the result a speedup. A useful benchmark compares baseline and speculative runs under the same conditions.&lt;/p&gt;

&lt;p&gt;Keep these fixed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Target model and quantization&lt;/li&gt;
&lt;li&gt;Hardware and GPU count&lt;/li&gt;
&lt;li&gt;Runtime version and container image&lt;/li&gt;
&lt;li&gt;Prompt dataset&lt;/li&gt;
&lt;li&gt;Prompt length distribution&lt;/li&gt;
&lt;li&gt;Output length limit&lt;/li&gt;
&lt;li&gt;Sampling parameters&lt;/li&gt;
&lt;li&gt;Concurrency&lt;/li&gt;
&lt;li&gt;Warm-up procedure&lt;/li&gt;
&lt;li&gt;Measurement window&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Record both decode throughput and request-level behavior. Depending on your use case, useful outputs include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Output tokens per second&lt;/li&gt;
&lt;li&gt;Time to first token&lt;/li&gt;
&lt;li&gt;End-to-end latency&lt;/li&gt;
&lt;li&gt;Accepted draft tokens per verification step&lt;/li&gt;
&lt;li&gt;Acceptance length&lt;/li&gt;
&lt;li&gt;GPU memory use&lt;/li&gt;
&lt;li&gt;Error rate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a small benchmark client that sends prompts concurrently and estimates generated output tokens. It is not a replacement for a full benchmark tool, but it is useful for a controlled smoke test.&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;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncOpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AsyncOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8000/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not-needed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;prompts&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;Write a Python function to merge two sorted lists.&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;Explain how a Kubernetes readiness probe differs from a liveness probe.&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;Show a PostgreSQL query that finds duplicate email addresses.&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen3.8-27B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&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;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;approx_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.3&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;approx_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&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="nf"&gt;run_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&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;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;total_seconds&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;elapsed&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;total_tokens&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;tokens&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&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="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Approximate aggregate throughput: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total_tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_seconds&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tokens/sec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For serious measurements, use the tokenizer rather than an approximate token count, use enough prompts to smooth out variance, and run the exact same harness against baseline and speculative servers.&lt;/p&gt;

&lt;p&gt;A result table should disclose enough context to be interpreted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Target model: Qwen3.8 27B
Drafter: custom DFlash checkpoint
Hardware: one GPU, exact model recorded in run metadata
Prompt set: held-out mixed coding and agent prompts
Temperature: 0
Max output tokens: 512
Concurrency: 1

Mode                    Output tokens/sec    Notes
Baseline                14.0                 No speculative decoding
DFlash via SGLang       18.5                 Runtime version recorded
DFlash via vLLM         20.0                 Runtime version recorded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The numbers themselves matter less than the methodology. If the runtime versions or kernels differ, say so. If you compiled a runtime from a pull request, say so. If the speculative path only helps at a particular concurrency or output length, show it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The run dies after an SSH disconnect
&lt;/h3&gt;

&lt;p&gt;Use tmux or a similar terminal multiplexer. Make data generation and checkpointing resumable. Keep logs on disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  You run out of disk
&lt;/h3&gt;

&lt;p&gt;Check disk space before and during the run:&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;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; /workspace/&lt;span class="k"&gt;*&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checkpoints, model caches, and hidden states can grow much faster than expected. Clean only files you fully understand. Never delete the latest known-good checkpoint until the export has loaded successfully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version mismatches
&lt;/h3&gt;

&lt;p&gt;Symptoms include broken imports, serialization failures, CUDA kernel errors, incompatible model classes, and runtime errors while serving an exported checkpoint.&lt;/p&gt;

&lt;p&gt;Fix this by pinning working versions, recording git commits, and rebuilding from a known-good environment. Avoid partial upgrades in the middle of an experiment.&lt;/p&gt;

&lt;h3&gt;
  
  
  High training accuracy but weak serving gain
&lt;/h3&gt;

&lt;p&gt;This is the classic drafter failure mode. Possible causes include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The training prompts do not match serving prompts.&lt;/li&gt;
&lt;li&gt;The drafter is too slow relative to its acceptance benefit.&lt;/li&gt;
&lt;li&gt;The draft block is too long for the model's quality level.&lt;/li&gt;
&lt;li&gt;You measured a training proxy rather than live acceptance.&lt;/li&gt;
&lt;li&gt;The serving runtime is not using the expected optimized speculative path.&lt;/li&gt;
&lt;li&gt;The benchmark is dominated by prefill or short responses rather than decode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat the entire pipeline as the system under optimization, not just the checkpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Serving works but results differ across runtimes
&lt;/h3&gt;

&lt;p&gt;vLLM and SGLang may have different implementations, kernels, versions, cache behavior, batching choices, and speculative support maturity. Benchmark each runtime separately, record exact versions, and avoid claiming a direct runtime comparison unless the setups are truly matched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical next steps
&lt;/h2&gt;

&lt;p&gt;Once the generic pipeline works, the most interesting improvement is data specialization.&lt;/p&gt;

&lt;p&gt;A general-purpose drafter learns from a broad prompt distribution. But a local assistant may spend most of its time doing only a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Coding and debugging&lt;/li&gt;
&lt;li&gt;Tool calling&lt;/li&gt;
&lt;li&gt;Shell commands&lt;/li&gt;
&lt;li&gt;Structured JSON responses&lt;/li&gt;
&lt;li&gt;Infrastructure troubleshooting&lt;/li&gt;
&lt;li&gt;A fixed set of system prompts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can log and sanitize your own requests and target outputs, deduplicate them, split them into train and held-out evaluation sets, and continue training a strong base drafter on that distribution.&lt;/p&gt;

&lt;p&gt;Be careful with privacy. Strip secrets, tokens, personal data, customer data, and proprietary code before any dataset leaves your trusted environment. Keep a fixed held-out set so you can distinguish real generalization from memorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final checklist
&lt;/h2&gt;

&lt;p&gt;Before publishing or relying on a custom drafter, verify the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Target and draft checkpoints are an intended compatible pair
[ ] Training and serving dependency versions are recorded
[ ] Prompt data resembles the real serving workload
[ ] Distillation is resumable and outputs have been validated
[ ] Chat template and assistant-only loss behavior have been tested
[ ] Checkpoints are backed up off the rented instance
[ ] Hugging Face export loads successfully
[ ] Baseline and speculative servers use the same benchmark harness
[ ] Benchmark settings are documented
[ ] You measure end-to-end throughput, not acceptance alone
[ ] You have tested held-out prompts, not only training-like examples
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/z-lab/dflash" rel="noopener noreferrer"&gt;DFlash project and code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://z-lab.ai/projects/dflash/" rel="noopener noreferrer"&gt;DFlash project page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/html/2602.06036" rel="noopener noreferrer"&gt;DFlash paper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/NVIDIA/Model-Optimizer/blob/main/examples/speculative_decoding/doc/dflash.md" rel="noopener noreferrer"&gt;NVIDIA Model Optimizer DFlash training documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sgl-project/sglang" rel="noopener noreferrer"&gt;SGLang&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vllm-project/vllm" rel="noopener noreferrer"&gt;vLLM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Training a drafter is a useful systems exercise because it touches the whole LLM stack: data design, target-model behavior, GPU infrastructure, distributed training, checkpoint formats, serving runtimes, and benchmark methodology.&lt;/p&gt;

&lt;p&gt;The best lesson is not that every model needs a custom drafter. It is that speculative decoding is only valuable when the complete system improves. Build a reliable pipeline first, measure the real workload, and then specialize the drafter where it makes sense.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>Speculative Decoding on Local Hardware: Benchmarking N-Gram, MTP, EAGLE3, and DFlash on the NVIDIA DGX Spark</title>
      <dc:creator>Kristiyan Stoyanov</dc:creator>
      <pubDate>Sat, 18 Jul 2026 18:20:03 +0000</pubDate>
      <link>https://dev.to/kstoyanovai/speculative-decoding-on-local-hardware-benchmarking-n-gram-mtp-eagle3-and-dflash-on-the-nvidia-2kec</link>
      <guid>https://dev.to/kstoyanovai/speculative-decoding-on-local-hardware-benchmarking-n-gram-mtp-eagle3-and-dflash-on-the-nvidia-2kec</guid>
      <description>&lt;h1&gt;
  
  
  Speculative Decoding on Local Hardware: Benchmarking N-Gram, MTP, EAGLE3, and DFlash on the NVIDIA DGX Spark
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; We benchmark four speculative decoding methods — N-Gram, MTP, EAGLE3, and DFlash — running &lt;code&gt;Qwen3.5-122B-A10B-hybrid-int4-fp8&lt;/code&gt; on a single NVIDIA DGX Spark (GB10, SM121). MTP-2 delivered the best balance of throughput and stability (~49 tok/s avg, ~51 tok/s peak). DFlash achieved the highest individual peaks (~78 tok/s) but with significant variance. Configurations shared.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why Speculative Decoding Matters for Local AI
&lt;/h2&gt;

&lt;p&gt;Squeezing every token per second out of expensive local hardware is not just a nice-to-have — on machines like the DGX Spark where you've invested in a system to run frontier-scale models locally, inference throughput directly translates to latency in agentic pipelines, coding assistants, and multi-turn conversations.&lt;/p&gt;

&lt;p&gt;Speculative decoding is one of the few optimization techniques that can deliver meaningful speedups &lt;strong&gt;without any change to output quality&lt;/strong&gt;. Because the target model's final verification step guarantees mathematically identical output distributions to standard autoregressive decoding, there is zero quality trade-off.&lt;/p&gt;




&lt;h2&gt;
  
  
  LLM Inference: The Memory-Bandwidth Bottleneck
&lt;/h2&gt;

&lt;p&gt;Before diving into speculative decoding mechanics, it's worth understanding &lt;em&gt;why&lt;/em&gt; LLM inference is slow in the first place.&lt;/p&gt;

&lt;p&gt;Every time an LLM generates a single token, the GPU must load the full model weight matrix from VRAM into compute units. For a 120B+ parameter model, this means moving tens of gigabytes of data — often for just a handful of floating-point operations per weight. This makes inference fundamentally &lt;strong&gt;memory-bandwidth bound&lt;/strong&gt;, not compute-bound. The GPU's tensor cores sit largely idle while the memory bus does the heavy lifting.&lt;/p&gt;

&lt;p&gt;Each inference step consists of two distinct phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prefill (Prompt Processing):&lt;/strong&gt; The input prompt tokens are processed in parallel. This phase is compute-bound and relatively fast, even for long contexts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decode (Generation):&lt;/strong&gt; The model generates one token at a time, autoregressively. Each step requires a full forward pass through the model, loading all weights from memory. This is the slow phase — and it scales linearly with output length.&lt;/li&gt;
&lt;/ol&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%2Ff9322s88l2xfs82hv3bq.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%2Ff9322s88l2xfs82hv3bq.png" alt=" " width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the DGX Spark with the &lt;code&gt;Qwen3.5-122B-A10B-hybrid-int4-fp8&lt;/code&gt; checkpoint, the unoptimized baseline sits around &lt;strong&gt;36–37 tok/s&lt;/strong&gt; generation throughput. That's the number we're trying to beat.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Speculative Decoding Works
&lt;/h2&gt;

&lt;p&gt;The core idea is to break the serial token-by-token bottleneck. A small, fast &lt;strong&gt;draft model&lt;/strong&gt; proposes a sequence of &lt;code&gt;K&lt;/code&gt; candidate tokens in rapid succession. The large &lt;strong&gt;target model&lt;/strong&gt; then verifies all &lt;code&gt;K&lt;/code&gt; tokens in a &lt;strong&gt;single forward pass&lt;/strong&gt; — because verification (running the full model over a known input) is significantly cheaper than independent generation.&lt;/p&gt;

&lt;p&gt;The effective speedup depends on two competing factors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Draft speed:&lt;/strong&gt; How fast the draft model can propose tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acceptance rate (α):&lt;/strong&gt; What fraction of draft tokens the target model accepts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A very cheap but low-quality drafter (low α) will generate tokens faster than the target can accept, wasting compute. A very accurate drafter that nearly matches the target model will have a high α but won't be meaningfully faster to run. The sweet spot is a &lt;strong&gt;lightweight drafter that accurately mimics the target model's distribution&lt;/strong&gt; for the task at hand.&lt;/p&gt;

&lt;p&gt;The expected number of tokens produced per target-model forward pass is:&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%2F7zvlbdgn1bch8nw3kzqz.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%2F7zvlbdgn1bch8nw3kzqz.png" alt=" " width="584" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;where &lt;code&gt;α&lt;/code&gt; is the per-token acceptance rate and &lt;code&gt;K&lt;/code&gt; is the number of speculative tokens. This means even modest acceptance rates (e.g., 0.8) with &lt;code&gt;K=2&lt;/code&gt; yield ~2.4 tokens per target pass — a meaningful multiplier.&lt;/p&gt;




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

&lt;p&gt;All tests were run on a single &lt;strong&gt;NVIDIA DGX Spark (GB10, SM121, 128 GB unified memory)&lt;/strong&gt; with the following base setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; &lt;code&gt;Qwen3.5-122B-A10B-hybrid-int4-fp8&lt;/code&gt; (Intel AutoRound INT4 MoE experts + FP8 dense shared expert layers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inference engine:&lt;/strong&gt; vLLM 0.19.1 compiled for SM121&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attention backend:&lt;/strong&gt; FlashInfer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MTP weights patch:&lt;/strong&gt; Required for the Intel AutoRound checkpoint (see below)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hybrid checkpoint is worth noting: the MoE expert weights are quantized to INT4 (via Intel's AutoRound), while the dense shared expert layers use FP8 from the official Qwen FP8 checkpoint. This hybrid approach avoids the accuracy penalty of INT4-quantizing high-utilization attention layers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/albond/DGX_Spark_Qwen3.5-122B-A10B-AR-INT4#step-1-build-hybrid-checkpoint-optional-9" rel="noopener noreferrer"&gt;Albond Recipe for Hybrid Checkpoint&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Method 1: N-Gram Speculative Decoding
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Concept:&lt;/strong&gt; N-Gram matching is the simplest possible speculative decoding approach — and the most instructive for understanding the acceptance rate dynamic. It uses no learned model at all. Instead, it scans the &lt;strong&gt;current context window&lt;/strong&gt; for recurring token n-grams and proposes continuations based on frequency co-occurrence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speculative configuration in vLLM:&lt;/strong&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ngram"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"num_speculative_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt_lookup_num_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;prompt_lookup_num_tokens&lt;/code&gt; controls how many tokens form the lookup key. The algorithm finds all positions in the context where the last &lt;code&gt;N&lt;/code&gt; tokens appeared before and proposes what came after them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it fails for open-ended generation:&lt;/strong&gt; N-gram matching works well when the output is highly repetitive relative to the input (e.g., the model is echoing back parts of the prompt, or doing summarization). For free-form generation, the context has no meaningful prefix matches, so the acceptance rate drops sharply.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; vllm-qwen35 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gpus&lt;/span&gt; all &lt;span class="nt"&gt;--net&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="nt"&gt;--ipc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /home/krisitown/AI/models:/models &lt;span class="se"&gt;\&lt;/span&gt;
  vllm-qwen35-v2 &lt;span class="se"&gt;\&lt;/span&gt;
  serve /models/qwen35-122b-hybrid-int4fp8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--served-model-name&lt;/span&gt; qwen/qwen3.5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-model-len&lt;/span&gt; 196608 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-num-batched-tokens&lt;/span&gt; 32768 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.88 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 8000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--load-format&lt;/span&gt; fastsafetensors &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attention-backend&lt;/span&gt; FLASHINFER &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-chunked-prefill&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-auto-tool-choice&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tool-call-parser&lt;/span&gt; qwen3_coder &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--generation-config&lt;/span&gt; auto &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--override-generation-config&lt;/span&gt; &lt;span class="s1"&gt;'{"temperature": 0.7, "top_p": 0.8, "top_k": 20, "presence_penalty": 0.0, "repetition_penalty": 1.0}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-config&lt;/span&gt; &lt;span class="s1"&gt;'{"method":"ngram","num_speculative_tokens":4,"prompt_lookup_min":2,"prompt_lookup_max":4}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb4t3dga7qqycomedhasl.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%2Fb4t3dga7qqycomedhasl.png" alt=" " width="799" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; ~24–30 tok/s — &lt;strong&gt;below the 36–37 tok/s baseline&lt;/strong&gt;. The overhead of proposing tokens that get rejected outweighs any gain. N-Gram speculative decoding is only practical in highly repetitive/retrieval-heavy workloads.&lt;/p&gt;




&lt;h2&gt;
  
  
  Method 2: MTP (Multi-Token Prediction)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Concept:&lt;/strong&gt; MTP is a fundamentally different approach. Rather than using a separate external draft model, MTP leverages a &lt;strong&gt;lightweight head that was trained jointly with the target model&lt;/strong&gt; and ships as part of its checkpoint. Because the MTP head has been optimized to match the target model's own output distribution — sharing its internal representations — acceptance rates are dramatically higher than any external drafter.&lt;/p&gt;

&lt;p&gt;The MTP head adds extra transformer layers at the end of the main model that are trained with an auxiliary loss to predict the &lt;em&gt;next&lt;/em&gt; token in the sequence, using the intermediate hidden states of the main model. During inference, these layers run as a cheap drafting pass that reuses the KV cache already computed by the main model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The MTP weights patch for Intel AutoRound:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Intel AutoRound INT4 checkpoint for Qwen3.5-122B ships the MTP weights in &lt;code&gt;model_extra_tensors.safetensors&lt;/code&gt; (785 tensors, ~4.8 GB BF16) but &lt;strong&gt;does not register them in &lt;code&gt;model.safetensors.index.json&lt;/code&gt;&lt;/strong&gt;. vLLM reads the index to discover weights, so it never loads the MTP head unless you patch the index manually. The recipe at &lt;a href="https://github.com/albond/DGX_Spark_Qwen3.5-122B-A10B-AR-INT4" rel="noopener noreferrer"&gt;albond/DGX_Spark_Qwen3.5-122B-A10B-AR-INT4&lt;/a&gt; includes a script that registers all 785 tensor mappings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python patches/02-mtp-speculative/add-mtp-weights.py &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INTEL_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--target&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MODEL_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this patch, adding &lt;code&gt;--speculative-config&lt;/code&gt; will silently have no effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vLLM configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm serve /models/qwen35-122b-hybrid-int4fp8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attention-backend&lt;/span&gt; FLASHINFER &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-config&lt;/span&gt; &lt;span class="s1"&gt;'{"method":"mtp","num_speculative_tokens":2}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ MTP requires &lt;code&gt;--attention-backend FLASHINFER&lt;/code&gt;. The PyTorch backend does not support MTP in vLLM ≤0.19.&lt;/p&gt;
&lt;/blockquote&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%2Fy20ibebx86ljt7nzbl28.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%2Fy20ibebx86ljt7nzbl28.png" alt=" " width="799" height="303"&gt;&lt;/a&gt;&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%2Fif688gj7h70ort3564vf.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%2Fif688gj7h70ort3564vf.png" alt=" " width="799" height="33"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tuning &lt;code&gt;num_speculative_tokens&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The number of speculative tokens is the key tuning parameter. Testing with 1, 2, and 4 tokens revealed a clear pattern:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;num_speculative_tokens&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Avg tok/s&lt;/th&gt;
&lt;th&gt;Peak tok/s&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;0 (baseline)&lt;/td&gt;
&lt;td&gt;~36–37&lt;/td&gt;
&lt;td&gt;~38&lt;/td&gt;
&lt;td&gt;No speculative decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~44–45&lt;/td&gt;
&lt;td&gt;~46&lt;/td&gt;
&lt;td&gt;Solid improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;~48–49&lt;/td&gt;
&lt;td&gt;~51&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Best balance&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;~34–35&lt;/td&gt;
&lt;td&gt;~38&lt;/td&gt;
&lt;td&gt;Acceptance rate drops, worse than baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The degradation at &lt;code&gt;num_speculative_tokens=4&lt;/code&gt; is expected. Each additional speculative token compounds the error: if the MTP head diverges from the target model at position &lt;code&gt;i&lt;/code&gt;, all subsequent positions &lt;code&gt;i+1 ... K&lt;/code&gt; are already wrong, so the overhead of generating and then discarding them hurts throughput. The acceptance rate at position 4 was measured in the low 50s%, meaning roughly half of fourth-position tokens were rejected — and the overhead of running the head + verification + token correction exceeded the benefit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; MTP-2 delivers ~49 tok/s average, ~51 tok/s peak — approximately &lt;strong&gt;+32% over baseline&lt;/strong&gt; with stable, consistent throughput.&lt;/p&gt;




&lt;h2&gt;
  
  
  Method 3: EAGLE3
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Concept:&lt;/strong&gt; EAGLE3 is the third iteration of the EAGLE family of speculative decoding drafters. Unlike MTP, the EAGLE drafter is a &lt;strong&gt;separately trained model&lt;/strong&gt; — but it compensates for this by conditioning on the target model's &lt;strong&gt;internal hidden states&lt;/strong&gt;, not just the output token embeddings.&lt;/p&gt;

&lt;p&gt;The intuition is: if the draft model has access to the target model's intermediate representations at the current position, it already "knows" most of what the target was going to compute before producing the next token. This dramatically improves acceptance rates compared to a purely external drafter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EAGLE3 architectural changes from EAGLE1/2:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Multi-layer hidden state inputs:&lt;/strong&gt; The drafter consumes hidden states from multiple layers of the target model (not just the final layer). Earlier layers carry richer semantic information that the final layer collapses into a logit distribution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removal of next-feature prediction loss:&lt;/strong&gt; EAGLE-2 trained the drafter to predict both the next token &lt;em&gt;and&lt;/em&gt; the next hidden state. EAGLE3 drops the hidden state prediction auxiliary loss, freeing model capacity for the actual goal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inference-aligned training data augmentation:&lt;/strong&gt; During training, the drafter is exposed to its own autoregressive rollouts (not ground-truth hidden states), reducing the distribution shift between training and inference.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Practical constraint:&lt;/strong&gt; EAGLE3 heads are model-specific and must be trained for a particular target model checkpoint. For this test, no EAGLE3 head was available for the 122B MoE model, so the test was conducted on a separate &lt;strong&gt;Qwen3-30B-A3B&lt;/strong&gt; baseline (~33 tok/s without speculative decoding) using an EAGLE3 head pulled from Hugging Face.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vLLM configuration:&lt;/strong&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eagle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"model-name/eagle3-head"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"num_speculative_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Results on Qwen3-30B-A3B:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;num_speculative_tokens&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Avg tok/s&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;0 (baseline)&lt;/td&gt;
&lt;td&gt;~33&lt;/td&gt;
&lt;td&gt;Qwen3-30B-A3B baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~38–40&lt;/td&gt;
&lt;td&gt;~+20% improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;~15–17&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Severe degradation&lt;/strong&gt; (8-11% acceptance rate)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;num_speculative_tokens=12&lt;/code&gt; result is instructive. Even though the EAGLE3 drafter was running at 85–86 tok/s draft speed, an acceptance rate of 8–11% means the target model was rejecting the vast majority of drafts. The net result was roughly &lt;strong&gt;halved&lt;/strong&gt; generation throughput compared to baseline.&lt;/p&gt;

&lt;p&gt;This is the fundamental constraint of autoregressive drafters: each additional speculative token must be generated &lt;em&gt;sequentially&lt;/em&gt; by the drafter. Drafting 12 tokens takes 12 forward passes through the draft model, and if most are rejected, you've spent significant time generating tokens the target will discard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; EAGLE3 with &lt;code&gt;num_speculative_tokens=1&lt;/code&gt; delivered a ~+20% improvement on the 30B-A3B model. The method scales poorly with higher speculative token counts unless the acceptance rate is very high (&amp;gt;85%).&lt;/p&gt;




&lt;h2&gt;
  
  
  Method 4: DFlash (Block Diffusion Speculative Decoding)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Concept:&lt;/strong&gt; DFlash is the most architecturally novel method in this comparison, and addresses the core limitation of autoregressive drafters: the serial bottleneck. Instead of generating draft tokens one at a time, DFlash uses a &lt;strong&gt;lightweight block diffusion model&lt;/strong&gt; that generates a block of up to 16 tokens &lt;strong&gt;in a single forward pass&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The method draws inspiration from image generation diffusion models. In image diffusion, all pixels are generated in parallel via iterative denoising. DFlash adapts this concept to token generation: the drafter generates an entire block of masked tokens simultaneously, denoised in a single step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The target model processes the prompt and extracts &lt;strong&gt;hidden context features&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;These features are injected directly into the &lt;strong&gt;KV cache&lt;/strong&gt; of the draft diffusion block&lt;/li&gt;
&lt;li&gt;The diffusion head generates K tokens in one forward pass, conditioned on the target model's deep representations&lt;/li&gt;
&lt;li&gt;The target model verifies the proposed block with a single forward pass&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key difference from EAGLE3: because the block is generated in parallel, the &lt;strong&gt;marginal cost of generating additional speculative tokens approaches zero&lt;/strong&gt;. Generating 16 tokens takes roughly the same time as generating 4. This means DFlash can use far higher speculative token counts without the exponential cost increase that punishes autoregressive drafters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance ceiling:&lt;/strong&gt; Published benchmarks from the original DFlash paper (Chen et al., arXiv:2602.06036) show over 6x lossless acceleration on Qwen3 models, delivering up to 2.5x higher speedup than EAGLE3 on reasoning-heavy tasks. On coding benchmarks, acceptance lengths of 6–7 tokens per step were reported — double what EAGLE3 achieves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vLLM configuration (DFlash):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm serve /models/qwen35-122b-hybrid-int4fp8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attention-backend&lt;/span&gt; FLASHINFER &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-config&lt;/span&gt; &lt;span class="s1"&gt;'{
    "method": "dflash",
    "model": "z-lab/Qwen3.5-122B-DFlash",
    "num_speculative_tokens": 16
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DFlash head for the Qwen3.5-122B model was created by ZLab and is available on Hugging Face. Setup requires following a specific GitHub recipe (link in video description).&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%2F8asoeoa4rkuj48pd9djb.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%2F8asoeoa4rkuj48pd9djb.png" alt=" " width="800" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observed behavior:&lt;/strong&gt; DFlash showed high throughput variance. When the acceptance rate was strong (high-structure output like code), peak throughput hit &lt;strong&gt;~78 tok/s&lt;/strong&gt; — significantly exceeding MTP-2. However, for general-purpose prompts, throughput fell to ~30–33 tok/s average, with peaks to ~50 tok/s.&lt;/p&gt;

&lt;p&gt;The variance is explained by DFlash's acceptance semantics. Unlike autoregressive drafters where individual token rejections are isolated, DFlash accepts a &lt;strong&gt;prefix&lt;/strong&gt; of the diffusion block. If the target model accepts tokens 1–3 but rejects token 4, tokens 5–16 are discarded entirely even if they would have been accepted. This means low acceptance rate at any position in the block wastes the entire remaining draft. When the task is highly structured (code, JSON, math), the acceptance rate is consistently high across positions, and DFlash dominates. For open-ended generation, variance is high.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; DFlash achieved the highest peak throughput (~78 tok/s) but also the highest variance. Average throughput for general prompts was lower than MTP-2. For structured-output tasks (coding agents, JSON generation), DFlash is compelling. It is also a relatively new method and actively improving.&lt;/p&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Avg tok/s&lt;/th&gt;
&lt;th&gt;Peak tok/s&lt;/th&gt;
&lt;th&gt;Acceptance Rate&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;None (baseline)&lt;/td&gt;
&lt;td&gt;Qwen3.5-122B&lt;/td&gt;
&lt;td&gt;~36–37&lt;/td&gt;
&lt;td&gt;~38&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;N-Gram&lt;/td&gt;
&lt;td&gt;Qwen3.5-122B&lt;/td&gt;
&lt;td&gt;~28–30&lt;/td&gt;
&lt;td&gt;~31&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;td&gt;Worse than baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP-1&lt;/td&gt;
&lt;td&gt;Qwen3.5-122B&lt;/td&gt;
&lt;td&gt;~44–45&lt;/td&gt;
&lt;td&gt;~47&lt;/td&gt;
&lt;td&gt;Mid-to-high 80s%&lt;/td&gt;
&lt;td&gt;Solid improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP-2&lt;/td&gt;
&lt;td&gt;Qwen3.5-122B&lt;/td&gt;
&lt;td&gt;~48–49&lt;/td&gt;
&lt;td&gt;~51&lt;/td&gt;
&lt;td&gt;~80%+&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Best consistency&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP-4&lt;/td&gt;
&lt;td&gt;Qwen3.5-122B&lt;/td&gt;
&lt;td&gt;~34–35&lt;/td&gt;
&lt;td&gt;~38&lt;/td&gt;
&lt;td&gt;Drops significantly&lt;/td&gt;
&lt;td&gt;Worse than MTP-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;None (baseline)&lt;/td&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td&gt;~33&lt;/td&gt;
&lt;td&gt;~40&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Separate model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAGLE3 (1 token)&lt;/td&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td&gt;~38–39&lt;/td&gt;
&lt;td&gt;~41&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;~20% improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DFlash&lt;/td&gt;
&lt;td&gt;Qwen3.5-122B&lt;/td&gt;
&lt;td&gt;~48–50 avg*&lt;/td&gt;
&lt;td&gt;~78&lt;/td&gt;
&lt;td&gt;Variable&lt;/td&gt;
&lt;td&gt;High peaks, high variance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*DFlash average varies significantly by task type (structured vs. open-ended).&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Guidance: Choosing the Right Method
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use MTP&lt;/strong&gt; if your model ships with a native MTP head (Qwen3.5, DeepSeek V3, Gemma 4). It is the lowest-friction option — no separate model download, no architecture mismatch risk, and acceptance rates are consistently high because the head was jointly trained. &lt;code&gt;num_speculative_tokens=2&lt;/code&gt; is a strong default for most Qwen3.5 deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use EAGLE3&lt;/strong&gt; if no MTP head is available and a well-matched EAGLE3 drafter exists for your target model on Hugging Face. Keep &lt;code&gt;num_speculative_tokens&lt;/code&gt; conservatively at 1–4; beyond that, acceptance rates drop sharply for most tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use DFlash&lt;/strong&gt; for structured-output workloads: code generation, function calling, JSON mode, math. The parallel drafting architecture gives DFlash a fundamental advantage when the output has strong syntactic structure and acceptance rates across the block are consistently high. Avoid it for open-ended chat where variance makes latency unpredictable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid N-Gram&lt;/strong&gt; for general inference. It is primarily useful as a pedagogical example or in specialized summarization/extraction pipelines where output closely mirrors input.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speculative decoding is lossless.&lt;/strong&gt; Output quality is mathematically identical to standard autoregressive decoding. The only variable is throughput.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acceptance rate is everything.&lt;/strong&gt; More speculative tokens are not always better. The acceptance rate at each position compounds — even a single bad position in an autoregressive chain invalidates all subsequent drafts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DFlash's parallel drafting breaks the serial constraint.&lt;/strong&gt; By generating blocks in a single forward pass, DFlash eliminates the per-token cost overhead that limits autoregressive drafters at high &lt;code&gt;K&lt;/code&gt;. This is the architectural reason its peak throughput is so high.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task type matters.&lt;/strong&gt; Structured output (code, JSON, math) consistently benefits more from speculative decoding than open-ended generation, because the conditional distribution is sharper and easier for any drafter to model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware matters.&lt;/strong&gt; These numbers are specific to the DGX Spark GB10 (SM121, 128 GB unified memory). Results on H100, A100, or consumer GPUs will differ due to memory bandwidth, compute, and kernel optimization differences.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want more detail, here is a YT video I did, going through the tests:   &lt;iframe src="https://www.youtube.com/embed/jCUHWeXehnI"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/albond/DGX_Spark_Qwen3.5-122B-A10B-AR-INT4" rel="noopener noreferrer"&gt;albond/DGX_Spark_Qwen3.5-122B-A10B-AR-INT4&lt;/a&gt; — Hybrid checkpoint + MTP patch recipe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2602.06036" rel="noopener noreferrer"&gt;DFlash paper: arXiv:2602.06036&lt;/a&gt; — Block Diffusion for Flash Speculative Decoding&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/z-lab" rel="noopener noreferrer"&gt;ZLab DFlash models on Hugging Face&lt;/a&gt; — DFlash heads for Qwen3 models&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.vllm.ai/en/latest/features/speculative_decoding/" rel="noopener noreferrer"&gt;vLLM Speculative Decoding docs&lt;/a&gt; — Configuration reference&lt;/li&gt;
&lt;li&gt;&lt;a href="https://publish.obsidian.md/shri/Appendix/Inference+Acceleration/EAGLE-3" rel="noopener noreferrer"&gt;EAGLE3 architecture reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>benchmark</category>
      <category>agents</category>
    </item>
    <item>
      <title>The AI agent habit that was quietly wasting my time and tokens</title>
      <dc:creator>Kristiyan Stoyanov</dc:creator>
      <pubDate>Tue, 23 Jun 2026 20:39:55 +0000</pubDate>
      <link>https://dev.to/kstoyanovai/the-ai-agent-habit-that-was-quietly-wasting-my-time-and-tokens-2jmi</link>
      <guid>https://dev.to/kstoyanovai/the-ai-agent-habit-that-was-quietly-wasting-my-time-and-tokens-2jmi</guid>
      <description>&lt;h1&gt;
  
  
  The AI Agent Habit That Was Quietly Wasting My Time and Tokens
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Tags: ai, localai, machinelearning, productivity, agents&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I realized I had been using AI agents in a way that looked smart but was actually pretty wasteful.&lt;/p&gt;

&lt;p&gt;The pattern was simple: I would ask the agent for something useful, it would go off and figure it out, and eventually I would get an answer. The problem is that if you keep asking the agent to rediscover the same process over and over, you are paying for repeated reasoning, repeated tool usage, and repeated trial and error. That means more tokens, more latency, and more opportunities for the agent to fumble.&lt;/p&gt;

&lt;p&gt;What finally clicked for me was this: use LLM inference for decisions, not for repetition.&lt;/p&gt;

&lt;p&gt;If a task has already been figured out once, I do not want the model burning context and tool calls to solve it again every time. I want the model to recognize the task, use a reliable tool, and move on.&lt;/p&gt;

&lt;p&gt;That is the pattern I have been using with Hermes, and it has made my local agent setup much more useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup I Am Running
&lt;/h2&gt;

&lt;p&gt;Right now I am running Hermes on a DGX Spark. In the video, I show the machine with 128 GB of unified memory, and at that moment I had about 1 GB free because I had a quantized Qwen 3.5 model loaded locally.&lt;/p&gt;

&lt;p&gt;Hermes is my current agent framework of choice. I have tried other options, but Hermes has been easy to install and easy to live with. One thing I especially like is that it supports Telegram through a gateway, so I can talk to my agent from my phone instead of only from a terminal window.&lt;/p&gt;

&lt;p&gt;On the tool side, the ones that matter most for this workflow are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web search and scraping&lt;/li&gt;
&lt;li&gt;Terminal access&lt;/li&gt;
&lt;li&gt;File operations&lt;/li&gt;
&lt;li&gt;Code execution&lt;/li&gt;
&lt;li&gt;Sub-agent delegation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For web search, I am using Tavily. In the video, I mention the free tier gives about 1,000 requests per month, which is enough for experimentation but still limited enough that I notice when an agent wastes calls.&lt;/p&gt;

&lt;p&gt;That matters, because this whole post is really about reducing unnecessary tool usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wasteful Version
&lt;/h2&gt;

&lt;p&gt;I started with a normal prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What is the weather going to be like in Sofia this weekend? Help me plan some activities based on it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is exactly the kind of thing I would send to an agent from Telegram while I am on the move.&lt;/p&gt;

&lt;p&gt;Hermes did eventually answer, but watching the trace was the important part. It did a web search, then a web extract, then checked time and date, then stumbled a bit, then searched again after not getting what it wanted the first time. In the video, I call out the real cost: this simple request filled about 20k tokens of context.&lt;/p&gt;

&lt;p&gt;And that is the issue.&lt;/p&gt;

&lt;p&gt;The answer was fine. The process was not.&lt;/p&gt;

&lt;p&gt;If I ask for weather and activity suggestions regularly, I do not want the model improvising a mini research project every single time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Better Version: Research Once, Automate Once, Reuse Forever
&lt;/h2&gt;

&lt;p&gt;Instead of asking the agent the end question again, I switched to building a capability.&lt;/p&gt;

&lt;p&gt;First, I asked Hermes to research free weather APIs that did not need a key and were easy to automate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Research free and open APIs that give you weather forecasts. Look for APIs that do not need an API key and can be easily automated with a Python script. Do not write the script yet. Let me choose the API first.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hermes went off, searched around, and came back with several options, including Open-Meteo, WeatherAPI, and met.no. It recommended Open-Meteo, and that was good enough for me.&lt;/p&gt;

&lt;p&gt;So I moved to the next step and told it to build something concrete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Let's use Open-Meteo. I want you to spawn an open code sub-agent and create a directory. Inside of that directory the sub-agent must implement an Open-Meteo API client wrapped by a CLI. Use Python. Make sure it uses real data. Use mocks only for unit tests. Report back when ready.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That “use real data, mocks only for unit tests” line is one I use a lot. If the agent can run against reality, it can verify its own work much better.&lt;/p&gt;

&lt;p&gt;Hermes delegated the task to a coding-focused sub-agent, created the project, and implemented the CLI.&lt;/p&gt;

&lt;p&gt;Then came the part that matters most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Never Trust the Agent
&lt;/h2&gt;

&lt;p&gt;When the agent said the project was complete, I did not just accept it.&lt;/p&gt;

&lt;p&gt;I tested it with a real request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Now let's test it with the real API. I want you to use the script to give me the weather forecast for Berlin.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the rule I keep coming back to: never trust your agent.&lt;/p&gt;

&lt;p&gt;Read the code. Run the script. Verify the output. Make sure it is using the real API. Make sure it is not doing anything unexpected. Only after that should it move from “experiment” to “capability.”&lt;/p&gt;

&lt;p&gt;In the demo, the script returned a 7-day Berlin forecast in about 0.4 seconds. That is the moment where the whole pattern becomes obvious. The slow, token-heavy part was discovering how to do the task. Once that is solved, the best move is to package it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning a One-Off Script into a Permanent Skill
&lt;/h2&gt;

&lt;p&gt;Once the weather CLI worked, I asked Hermes to wrap it as a reusable skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Now let's create a skill for you that wraps around this CLI script and uses it whenever I ask you about the weather in future sessions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hermes created the skill using its skill management flow, and that became part of its permanent skill set.&lt;/p&gt;

&lt;p&gt;Then I started a completely new session.&lt;/p&gt;

&lt;p&gt;That is the real test, because a fresh session has fresh context. No hidden memory from the earlier chat. No cheating.&lt;/p&gt;

&lt;p&gt;I asked the same question again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What is the weather going to be like in Sofia this weekend? Help me plan some activities based on it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time Hermes checked its skills, found the weather skill, executed the script, and gave me a clean answer with activity suggestions.&lt;/p&gt;

&lt;p&gt;The difference was huge.&lt;/p&gt;

&lt;p&gt;The first time, it burned through web searches, including two Tavily searches, and spent a lot of tokens figuring out how to answer. The second time, it reduced the whole thing to essentially one tool call to the script I had already verified.&lt;/p&gt;

&lt;p&gt;That is the pattern in one line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Explore once. Automate once. Wrap it as a skill. Reuse forever.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  This Gets More Interesting Than Weather
&lt;/h2&gt;

&lt;p&gt;Weather is a toy example, but it is useful because the waste is easy to see.&lt;/p&gt;

&lt;p&gt;The more interesting example from the video is one I built from my phone over Telegram. I use my agent pretty often for stock-related questions, so I had it create a stock analyzer script that fetches stock or index data from an open API.&lt;/p&gt;

&lt;p&gt;Same pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a stock analyzer Python script that fetches stock or index data from a popular and open API. Use real data. Use mocks only for unit tests.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When it finished, I verified it with a real run for Microsoft one year back. The output included about 250 trading days of data, the latest price, some moving averages, technical indicators, and a short interpretation.&lt;/p&gt;

&lt;p&gt;Then I turned that into a skill too.&lt;/p&gt;

&lt;p&gt;In a brand-new session, I asked a vague question about the USO ETF. I did not mention the script. I did not explain the workflow again. Hermes picked the stock analyzer skill on its own and returned a useful summary with current data.&lt;/p&gt;

&lt;p&gt;That is where this starts to feel less like chatting with a model and more like growing a personal assistant over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Security Rule I Want to Keep
&lt;/h2&gt;

&lt;p&gt;The core safety idea here is simple: verify before you automate.&lt;/p&gt;

&lt;p&gt;If an agent writes a script, read it. If it claims something works, test it. If it needs access to real systems, expose only the operations you actually want it to perform.&lt;/p&gt;

&lt;p&gt;For anything sensitive, I would keep the agent on the narrowest possible rails. In practice, that means preferring read-only capabilities where possible, using small purpose-built tools instead of broad access, and only promoting a workflow into a permanent skill after I have seen it behave correctly.&lt;/p&gt;

&lt;p&gt;The more capable the agent gets, the more important this becomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;What I like most about this pattern is that it compounds.&lt;/p&gt;

&lt;p&gt;Every time I notice a repeated agent task, I have a choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep paying for the agent to rediscover the solution.&lt;/li&gt;
&lt;li&gt;Or turn the solution into a reusable capability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over time, that changes the shape of the whole setup.&lt;/p&gt;

&lt;p&gt;I stop treating the model like a universal improviser and start treating it like a coordinator that knows when to call reliable tools. The model still provides the intelligence, but the repetitive parts move into code.&lt;/p&gt;

&lt;p&gt;That opens the door to more domain-specific assistants too. A natural next step is something like a private realtor assistant that checks listings, pulls mortgage news, summarizes changes, and sends a Telegram update on a schedule. Same principle, just applied to a workflow that actually matters to someone day to day.&lt;/p&gt;

&lt;p&gt;That is the part I find exciting. Not AI magic, but a steadily improving assistant that gets more useful because I keep teaching it durable skills.&lt;/p&gt;

&lt;p&gt;If you want to see the full walkthrough, including the Hermes session, the weather skill build, and the Telegram-based stock example, watch the YouTube video here:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/ZmYmY91rMkE"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;If you are experimenting with agents locally, I would love to hear what repeated task you would turn into a skill first.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>agentskills</category>
      <category>hermes</category>
    </item>
  </channel>
</rss>
