<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://shekswess.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://shekswess.github.io/" rel="alternate" type="text/html" /><updated>2026-07-17T10:32:54+00:00</updated><id>https://shekswess.github.io/feed.xml</id><title type="html">Bojan Jakimovski</title><author><name>Bojan Jakimovski</name></author><entry><title type="html">Benchmarking Grok 4.3 on Amazon Bedrock Mantle vs. the xAI API: The Engineering Walkthrough</title><link href="https://shekswess.github.io/grok-4-3-engineering-walkthrough.html" rel="alternate" type="text/html" title="Benchmarking Grok 4.3 on Amazon Bedrock Mantle vs. the xAI API: The Engineering Walkthrough" /><published>2026-07-16T22:00:00+00:00</published><updated>2026-07-16T22:00:00+00:00</updated><id>https://shekswess.github.io/grok-4-3-engineering-walkthrough</id><content type="html" xml:base="https://shekswess.github.io/grok-4-3-engineering-walkthrough.html"><![CDATA[<h1 id="benchmarking-grok-43-on-amazon-bedrock-mantle-vs-the-xai-api-the-engineering-walkthrough-with-loka">Benchmarking Grok 4.3 on Amazon Bedrock Mantle vs. the xAI API: The Engineering Walkthrough with Loka</h1>

<p><strong>Written by</strong> Nina Cvetkovska <strong>and</strong> <a href="https://medium.com/@jakimovski_bojan">Bojan Jakimovski</a></p>

<p>Original source: <a href="/the-many-paths-to-grok-4-3.html">The Many Paths to Grok 4.3</a> · <a href="https://medium.com/loka-engineering/benchmarking-grok-4-3-07ecfa9a48f3">Medium engineering guide</a></p>

<p><img src="/assets/images/grok-benchmark/background.png" alt="Grok 4.3 benchmark illustration showing Amazon Bedrock Mantle and the xAI API paths" /></p>

<h2 id="the-engineering-walkthrough">The Engineering Walkthrough</h2>

<p>The main post covers what we found: Grok 4.3 lands in the same 96–97% accuracy band on GSM8K whether you reach it through Amazon Bedrock Mantle or the first-party xAI API, and <code class="language-plaintext highlighter-rouge">low</code> reasoning effort captures most of the accuracy gain at a fraction of the latency and token cost of <code class="language-plaintext highlighter-rouge">medium</code> or <code class="language-plaintext highlighter-rouge">high</code>. This post covers how we ran it. If you want to reproduce the numbers or adapt the harness for your own workload, this is the companion piece.</p>

<p>The concrete question behind the benchmark was simple:</p>

<blockquote>
  <p><em>Does routing Grok 4.3 through Amazon Bedrock Mantle cost you anything in answer quality, latency, or reliability compared to the native xAI API?</em></p>
</blockquote>

<p>We ran the full GSM8K test split (1,319 grade-school math problems) through both provider paths at four reasoning-effort levels (<code class="language-plaintext highlighter-rouge">none</code>, <code class="language-plaintext highlighter-rouge">low</code>, <code class="language-plaintext highlighter-rouge">medium</code>, <code class="language-plaintext highlighter-rouge">high</code>), recording per-request streaming latency, output throughput, token usage, and failures. That is eight runs and 10,552 scored requests in total. The benchmark harness is open and the results are saved in the repo. This post walks through the setup, the exact commands, the output format, and the design choices that made the comparison clean.</p>

<p>For the main findings (accuracy tables, latency charts, and the enterprise deployment read), see the main Loka writeup: <a href="/the-many-paths-to-grok-4-3.html">The Many Paths to Grok 4.3</a>.</p>

<h2 id="why-gsm8k">Why GSM8K</h2>

<p>We needed a dataset that was long enough to produce stable latency distributions (1,319 requests per run means the p95 figures mean something), short enough to complete in a single session without batching or parallelism, and one where answer correctness is unambiguous and automatable. GSM8K satisfies all three. The final <code class="language-plaintext highlighter-rouge">####</code> answer format makes accuracy scoring a string comparison after normalization, with no LLM judge in the loop. The harness pulls the split directly from the raw JSONL files in the openai/grade-school-math GitHub repository, so there is no dataset-library dependency to pin.</p>

<p>The math content also makes token budgets predictable. Each question fits in well under a hundred tokens, and answers are a few lines of working plus a numeric result. That keeps the benchmark from inflating latency through unusual output lengths and keeps costs bounded before you start. It also gave us a clean lens on Grok 4.3’s configurable reasoning effort: when the task is this uniform, any accuracy or latency difference between effort levels is easy to attribute.</p>

<h2 id="tested-setup">Tested Setup</h2>

<p>The measured runs used:</p>

<table>
  <thead>
    <tr>
      <th>Setting</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bedrock model ID</td>
      <td><code class="language-plaintext highlighter-rouge">xai.grok-4.3</code></td>
    </tr>
    <tr>
      <td>Bedrock endpoint</td>
      <td>Bedrock Mantle, <code class="language-plaintext highlighter-rouge">us-west-2</code> (<code class="language-plaintext highlighter-rouge">https://bedrock-mantle.us-west-2.api.aws/openai/v1</code>)</td>
    </tr>
    <tr>
      <td>xAI model ID</td>
      <td><code class="language-plaintext highlighter-rouge">grok-4.3</code></td>
    </tr>
    <tr>
      <td>xAI endpoint</td>
      <td><code class="language-plaintext highlighter-rouge">https://api.x.ai/v1</code></td>
    </tr>
    <tr>
      <td>Reasoning efforts</td>
      <td><code class="language-plaintext highlighter-rouge">none</code>, <code class="language-plaintext highlighter-rouge">low</code>, <code class="language-plaintext highlighter-rouge">medium</code>, <code class="language-plaintext highlighter-rouge">high</code></td>
    </tr>
    <tr>
      <td>Dataset</td>
      <td>GSM8K test split, 1,319 questions</td>
    </tr>
    <tr>
      <td>Concurrency</td>
      <td>1 (sequential)</td>
    </tr>
    <tr>
      <td>Max output tokens</td>
      <td>4096</td>
    </tr>
    <tr>
      <td>Streaming</td>
      <td>enabled</td>
    </tr>
    <tr>
      <td>Auth (Bedrock)</td>
      <td>AWS SigV4 via an SSO profile, signed for the <code class="language-plaintext highlighter-rouge">bedrock-mantle</code> service (bearer tokens also supported)</td>
    </tr>
    <tr>
      <td>Auth (xAI)</td>
      <td>Bearer token via <code class="language-plaintext highlighter-rouge">XAI_API_KEY</code></td>
    </tr>
  </tbody>
</table>

<p>Sequential requests were a deliberate choice. Client-side concurrency is a real variable in production, but it conflates queue effects with the underlying provider infrastructure. Running one request at a time isolates the thing we were measuring: what does each provider’s serving path cost a single in-flight request?</p>

<p>One more Grok-specific note on the setup: reasoning in Grok 4.3 is always active by design, and the <code class="language-plaintext highlighter-rouge">reasoning.effort</code> parameter controls the budget, with <code class="language-plaintext highlighter-rouge">none</code> disabling it. The harness sends <code class="language-plaintext highlighter-rouge">"reasoning": {"effort": "&lt;level&gt;"}</code> in the request body on every run, including the <code class="language-plaintext highlighter-rouge">none</code> runs, where the value is passed through explicitly. It does not request reasoning summaries or encrypted reasoning content, so reasoning shows up only as a token count in the usage data, never as visible stream text.</p>

<h2 id="bootstrap-the-environment">Bootstrap the Environment</h2>

<p>The repo uses <code class="language-plaintext highlighter-rouge">uv</code> for dependency management and requires Python 3.12 or newer.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/NineCvet/grok-bedrock-xapi-benchmark.git
cd grok-bedrock-xapi-benchmark
uv sync
</code></pre></div></div>

<p>This installs the four runtime dependencies: <code class="language-plaintext highlighter-rouge">httpx</code> (streaming HTTP client), <code class="language-plaintext highlighter-rouge">boto3</code> (SigV4 signing via botocore), <code class="language-plaintext highlighter-rouge">python-dotenv</code> (loads the <code class="language-plaintext highlighter-rouge">.env</code> file), and <code class="language-plaintext highlighter-rouge">tiktoken</code> (token counting in the cost estimator). The dev group brings in <code class="language-plaintext highlighter-rouge">pytest</code> and <code class="language-plaintext highlighter-rouge">ruff</code>; <code class="language-plaintext highlighter-rouge">uv run pytest</code> and <code class="language-plaintext highlighter-rouge">uv run ruff check scripts tests</code> are the repo’s checks.</p>

<h2 id="configure-authentication">Configure Authentication</h2>

<p>The benchmark script supports two auth modes for Bedrock Mantle and picks between them automatically: if a bearer key is present in the environment it uses that, otherwise it falls back to SigV4 request signing with whatever AWS credentials botocore can resolve.</p>

<h3 id="bedrock-aws-sigv4-what-we-used">Bedrock: AWS SigV4 (what we used)</h3>

<p>Our runs used an AWS SSO profile. Log in, then pass the profile to the script through the standard <code class="language-plaintext highlighter-rouge">AWS_PROFILE</code> variable:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws sso login --profile loka-ml
</code></pre></div></div>

<p>The harness signs each request with SigV4 for the <code class="language-plaintext highlighter-rouge">bedrock-mantle</code> service in the target region. The default endpoint is <code class="language-plaintext highlighter-rouge">https://bedrock-mantle.&lt;region&gt;.api.aws/openai/v1</code>; override it with <code class="language-plaintext highlighter-rouge">BEDROCK_GROK_BASE_URL</code> if you need to.</p>

<p>Before spending anything, verify the auth path with <code class="language-plaintext highlighter-rouge">--auth-debug</code>. It builds and signs a request but never sends it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AWS_PROFILE=loka-ml uv run python scripts/benchmark_grok_latency.py \
  --provider bedrock \
  --auth-debug
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>auth_mode=sigv4
base_url=https://bedrock-mantle.us-west-2.api.aws/openai/v1
region=us-west-2
sigv4_service=bedrock-mantle
signed_header_names=Authorization, Content-Type, X-Amz-Date, X-Client-Request-Id
</code></pre></div></div>

<h3 id="bedrock-bearer-token">Bedrock: bearer token</h3>

<p>If you have a Bedrock API key instead, set <code class="language-plaintext highlighter-rouge">BEDROCK_GROK_API_KEY</code> (or <code class="language-plaintext highlighter-rouge">AWS_BEARER_TOKEN_BEDROCK</code>); when either is present it takes precedence over SigV4 and the script sends a plain <code class="language-plaintext highlighter-rouge">Authorization: Bearer</code> header. The same <code class="language-plaintext highlighter-rouge">--auth-debug</code> command confirms which mode is active.</p>

<h3 id="xai-api">xAI API</h3>

<p>There is no committed env template; create a <code class="language-plaintext highlighter-rouge">.env</code> in the repo root with your key:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat &gt; .env &lt;&lt;'EOF'
XAI_API_KEY=xai-your-key-here
EOF
</code></pre></div></div>

<p>The script loads it automatically at startup. The default endpoint is <code class="language-plaintext highlighter-rouge">https://api.x.ai/v1</code>, overridable with <code class="language-plaintext highlighter-rouge">XAI_BASE_URL</code>. The <code class="language-plaintext highlighter-rouge">.env</code> file is excluded from version control. Never commit it.</p>

<h2 id="estimate-cost-before-running">Estimate Cost Before Running</h2>

<p>The repo has a standalone estimator that downloads the GSM8K split, counts input tokens with <code class="language-plaintext highlighter-rouge">tiktoken</code>, and projects cost from a configurable output-token budget:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python scripts/estimate_grok_eval_cost.py \
  --dataset gsm8k \
  --split test \
  --model xai.grok-4.3 \
  --visible-output-tokens 512 \
  --reasoning-output-tokens 1024 \
  --output-dir result/cost-estimates
</code></pre></div></div>

<p>Its output (this is the real pre-run estimate saved in the repo):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>| Model          | Samples | Avg input tok | P95 input tok | Output tok/sample | Standard cost | Batch cost |
| -------------- | ------: | ------------: | ------------: | ----------------: | ------------: | ---------: |
| `xai.grok-4.3` |    1319 |          93.5 |           136 |             1,536 |       $5.2191 |    $2.6095 |
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">--reasoning-output-tokens</code> figure is deliberately an estimate: Grok bills reasoning tokens as output tokens even though they are not visible in responses, so the ceiling depends on the effort level you choose. The $5.22 projection assumed every request burns the full budget; the actual runs came in well under it because most GSM8K answers finish in a few hundred tokens. The realized totals from the result files: Bedrock runs cost $0.4418 (<code class="language-plaintext highlighter-rouge">none</code>) to $2.4400 (<code class="language-plaintext highlighter-rouge">high</code>), and xAI runs cost $0.5883 (<code class="language-plaintext highlighter-rouge">none</code>) to $2.2431 (<code class="language-plaintext highlighter-rouge">high</code>).</p>

<h2 id="run-the-latency-benchmark">Run the Latency Benchmark</h2>

<h3 id="bedrock-mantle-one-reasoning-effort">Bedrock Mantle (one reasoning effort)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AWS_PROFILE=loka-ml uv run python scripts/benchmark_grok_latency.py \
  --provider bedrock \
  --region us-west-2 \
  --model xai.grok-4.3 \
  --dataset gsm8k \
  --split test \
  --warmups 1 \
  --reasoning-effort low \
  --max-output-tokens 4096 \
  --concurrency 1 \
  --timeout-seconds 360 \
  --max-attempts 0 \
  --retry-backoff-seconds 2 \
  --retry-policy all-retryable \
  --max-billable-tokens 750000 \
  --max-estimated-grok-cost-usd 5.00 \
  --output-dir result/bedrock-grok43-low
</code></pre></div></div>

<p>The retry flags matter on the Bedrock path, for reasons covered in the reliability section below: <code class="language-plaintext highlighter-rouge">--retry-policy all-retryable</code> treats a provider response that explicitly asks for a retry as retryable, and <code class="language-plaintext highlighter-rouge">--max-attempts 0</code> retries until the request goes through, so the run finishes with a complete 1,319-row dataset.</p>

<h3 id="xai-api-one-reasoning-effort">xAI API (one reasoning effort)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python scripts/benchmark_grok_latency.py \
  --provider xai \
  --model grok-4.3 \
  --dataset gsm8k \
  --split test \
  --warmups 1 \
  --reasoning-effort low \
  --max-output-tokens 4096 \
  --concurrency 1 \
  --timeout-seconds 360 \
  --max-attempts 3 \
  --retry-backoff-seconds 2 \
  --max-billable-tokens 750000 \
  --max-estimated-grok-cost-usd 5.00 \
  --output-dir result/xai-grok43-low
</code></pre></div></div>

<p>Change the reasoning-effort flag to <code class="language-plaintext highlighter-rouge">none</code>, <code class="language-plaintext highlighter-rouge">medium</code>, or <code class="language-plaintext highlighter-rouge">high</code> and update the output directory accordingly for the other runs.</p>

<h3 id="what-the-eight-main-commands-were">What the eight main commands were</h3>

<p>The eight benchmark runs that produced the numbers in the main post used these output folders:</p>

<table>
  <thead>
    <tr>
      <th>Effort</th>
      <th>Provider</th>
      <th>Output folder</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>none</td>
      <td>Bedrock</td>
      <td><code class="language-plaintext highlighter-rouge">result/bedrock-grok43-none</code></td>
    </tr>
    <tr>
      <td>low</td>
      <td>Bedrock</td>
      <td><code class="language-plaintext highlighter-rouge">result/bedrock-grok43-low</code></td>
    </tr>
    <tr>
      <td>medium</td>
      <td>Bedrock</td>
      <td><code class="language-plaintext highlighter-rouge">result/bedrock-grok43-medium</code></td>
    </tr>
    <tr>
      <td>high</td>
      <td>Bedrock</td>
      <td><code class="language-plaintext highlighter-rouge">result/bedrock-grok43-high</code></td>
    </tr>
    <tr>
      <td>none</td>
      <td>xAI</td>
      <td><code class="language-plaintext highlighter-rouge">result/xai-grok43-none</code></td>
    </tr>
    <tr>
      <td>low</td>
      <td>xAI</td>
      <td><code class="language-plaintext highlighter-rouge">result/xai-grok43-low</code></td>
    </tr>
    <tr>
      <td>medium</td>
      <td>xAI</td>
      <td><code class="language-plaintext highlighter-rouge">result/xai-grok43-medium</code></td>
    </tr>
    <tr>
      <td>high</td>
      <td>xAI</td>
      <td><code class="language-plaintext highlighter-rouge">result/xai-grok43-high</code></td>
    </tr>
  </tbody>
</table>

<p>There is one extra folder next to these: <code class="language-plaintext highlighter-rouge">result/bedrock-grok43-low-error-examples</code> holds the partial JSONL from an early Bedrock <code class="language-plaintext highlighter-rouge">low</code> pass that we stopped after ten measured prompts. It exists on purpose, as raw evidence for the reliability section below.</p>

<h2 id="what-the-harness-does-during-a-run">What the Harness Does During a Run</h2>

<p>Each request goes through <code class="language-plaintext highlighter-rouge">run_once</code>, which opens a streaming connection and measures three timestamps:</p>

<ul>
  <li><strong>TTFB</strong> (time to first byte): the moment the first non-empty line arrives from the server, before any token is parsed. This is infrastructure only: connection setup, auth, admission, routing.</li>
  <li><strong>TTFT</strong> (time to first token): the moment the first visible output token event arrives. With reasoning enabled this includes the model’s thinking time before the first output character, which is why the main post’s TTFT numbers diverge so sharply between the two paths at <code class="language-plaintext highlighter-rouge">low</code> effort (3.14s on Bedrock vs 0.71s on xAI). Concretely, the harness marks TTFT on the first streamed <code class="language-plaintext highlighter-rouge">response.output_text.delta</code> event that carries text.</li>
  <li><strong>Total latency</strong>: the moment the stream closes.</li>
</ul>

<p>Token usage (input, output, and reasoning tokens separately) comes from the <code class="language-plaintext highlighter-rouge">usage</code> object on the stream’s completion event, where reasoning tokens sit under <code class="language-plaintext highlighter-rouge">output_tokens_details.reasoning_tokens</code>. Output throughput is derived per request as output tokens divided by total stream duration.</p>

<p>Each completed or failed record is written to a partial JSONL file immediately, so a crash partway through does not lose the records collected so far.</p>

<p>The live progress line looks like this (a real line from the Bedrock <code class="language-plaintext highlighter-rouge">low</code> run, reconstructed from the saved record):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bedrock xai.grok-4.3 run 44/1320: ok; in=151 out=307 reasoning=258; cost=$0.000956; cumulative_tokens=21,509; cumulative_est_grok_cost=$0.046901
</code></pre></div></div>

<p>Three run guards keep a misbehaving run from burning budget: <code class="language-plaintext highlighter-rouge">--max-billable-tokens</code> and <code class="language-plaintext highlighter-rouge">--max-estimated-grok-cost-usd</code> stop the run when cumulative usage crosses a cap (we set 750,000 tokens and $5.00), and <code class="language-plaintext highlighter-rouge">--timeout-seconds</code> bounds any single request (we set 360).</p>

<h2 id="output-files">Output Files</h2>

<p>Each run writes three files into the output directory, plus the partial JSONL that accumulates during the run. The accuracy scorer adds two more later. This is the real Bedrock <code class="language-plaintext highlighter-rouge">low</code> folder:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>result/bedrock-grok43-low/
├── grok_responses_latency_20260621T185415Z_partial.jsonl
├── grok_responses_latency_20260621T225049Z.jsonl
├── grok_responses_latency_20260621T225049Z_summary.json
├── grok_responses_latency_20260621T225049Z.md
├── gsm8k_accuracy_summary.json   (written later by the scorer)
└── gsm8k_accuracy_summary.md     (written later by the scorer)
</code></pre></div></div>

<p>The JSONL file has one record per request. This is a real record from the Bedrock <code class="language-plaintext highlighter-rouge">low</code> run, with the request ID shortened:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"attempts"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"error"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
  </span><span class="nl">"estimated_grok_cost_usd"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.000697</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expected_answer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"It takes 2/2=&lt;&lt;2/2=1&gt;&gt;1 bolt of white fiber</span><span class="se">\n</span><span class="s2">So the total amount of fabric is 2+1=&lt;&lt;2+1=3&gt;&gt;3 bolts of fabric</span><span class="se">\n</span><span class="s2">#### 3"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"input_tokens"</span><span class="p">:</span><span class="w"> </span><span class="mi">98</span><span class="p">,</span><span class="w">
  </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"xai.grok-4.3"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ok"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"output_chars"</span><span class="p">:</span><span class="w"> </span><span class="mi">125</span><span class="p">,</span><span class="w">
  </span><span class="nl">"output_chars_per_second"</span><span class="p">:</span><span class="w"> </span><span class="mf">77.96</span><span class="p">,</span><span class="w">
  </span><span class="nl">"output_text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Blue fiber requires 2 bolts. White fiber is half of that amount, or 1 bolt.  </span><span class="se">\n</span><span class="s2">Adding both gives a total of 3 bolts.  </span><span class="se">\n\n</span><span class="s2">#### 3"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"output_tokens"</span><span class="p">:</span><span class="w"> </span><span class="mi">230</span><span class="p">,</span><span class="w">
  </span><span class="nl">"output_tokens_per_second"</span><span class="p">:</span><span class="w"> </span><span class="mf">143.45</span><span class="p">,</span><span class="w">
  </span><span class="nl">"prompt_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gsm8k-test-1"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"prompt_index"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"prompt_text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Solve this grade-school math problem. Show concise reasoning, then put the final numeric answer on a separate line prefixed with ####.</span><span class="se">\n\n</span><span class="s2">Problem:</span><span class="se">\n</span><span class="s2">A robe takes 2 bolts of blue fiber and half that much white fiber.  How many bolts in total does it take?"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bedrock"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"reasoning_tokens"</span><span class="p">:</span><span class="w"> </span><span class="mi">186</span><span class="p">,</span><span class="w">
  </span><span class="nl">"request_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"fe647a91-..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"retry_errors"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
  </span><span class="nl">"run_index"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
  </span><span class="nl">"started_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-06-21T18:54:26.516990+00:00"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"total_ms"</span><span class="p">:</span><span class="w"> </span><span class="mf">1603.32</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ttfb_ms"</span><span class="p">:</span><span class="w"> </span><span class="mf">260.6</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ttft_ms"</span><span class="p">:</span><span class="w"> </span><span class="mf">1381.7</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The summary JSON has three top-level keys: <code class="language-plaintext highlighter-rouge">summary</code> (one row per provider and model with avg, p50, p90, and p95 for each metric), <code class="language-plaintext highlighter-rouge">aggregates</code> (LLMPerf-style blocks with the full distributions: p25, p50, p75, p90, p95, p99, mean, min, max, stddev for TTFB, TTFT, end-to-end latency, throughput, and the three token counts, plus overall throughput, completed requests per minute, and total estimated cost), and <code class="language-plaintext highlighter-rouge">dataset</code>. The Markdown file renders the same content as tables.</p>

<h2 id="score-accuracy-offline">Score Accuracy Offline</h2>

<p>The accuracy scorer is a separate script that never touches the API. It works entirely from the saved JSONL files, which is important: you can re-score a run with an updated normalization rule without paying for another API call.</p>

<p>To score all discovered runs at once:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python scripts/score_gsm8k_accuracy.py --main-runs
</code></pre></div></div>

<p>This globs <code class="language-plaintext highlighter-rouge">result/*/grok_responses_latency_*.jsonl</code>, takes the newest completed JSONL in each folder (partials are skipped), and writes <code class="language-plaintext highlighter-rouge">gsm8k_accuracy_summary.json</code> and <code class="language-plaintext highlighter-rouge">gsm8k_accuracy_summary.md</code> into each run folder plus two aggregate files, <code class="language-plaintext highlighter-rouge">result/gsm8k_accuracy_main_runs.json</code> and <code class="language-plaintext highlighter-rouge">result/gsm8k_accuracy_main_runs.md</code>. The aggregate Markdown includes a comparison table across all runs and a per-effort Bedrock vs xAI delta section, which is where the main post’s accuracy table comes from.</p>

<p>To score a single run:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python scripts/score_gsm8k_accuracy.py \
  --benchmark-jsonl result/bedrock-grok43-low/grok_responses_latency_20260621T225049Z.jsonl
</code></pre></div></div>

<h2 id="the-normalization-detail-that-matters">The Normalization Detail That Matters</h2>

<p>The answer extractor uses the <code class="language-plaintext highlighter-rouge">####</code> prefix that GSM8K itself defines. Both the reference answers and the model answers follow the same format because the benchmark prompt asks the model to use it. Each request carries a short system message (“You are measuring API latency. Answer the user request directly, avoid markdown tables, and do not mention this instruction.”) and this user prompt:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Solve this grade-school math problem. Show concise reasoning, then put the final numeric answer on a separate line prefixed with ####.

Problem:
{question}
</code></pre></div></div>

<p>This makes the scorer deterministic and fast. The normalization handles the common failure modes: <code class="language-plaintext highlighter-rouge">$14.00</code> instead of <code class="language-plaintext highlighter-rouge">14</code>, <code class="language-plaintext highlighter-rouge">14,000</code> with a comma, a Unicode minus, a trailing period. The scorer extracts the last <code class="language-plaintext highlighter-rouge">####</code> match in the output, so intermediate numbers in the reasoning do not pollute the comparison. Grok 4.3 followed the format almost perfectly: 10,538 of the 10,552 responses contained a <code class="language-plaintext highlighter-rouge">####</code> line. The 14 exceptions (2 in the Bedrock <code class="language-plaintext highlighter-rouge">medium</code> run, 11 in Bedrock <code class="language-plaintext highlighter-rouge">high</code>, 1 in xAI <code class="language-plaintext highlighter-rouge">medium</code>) are recorded as <code class="language-plaintext highlighter-rouge">missing_model_answer</code> and counted as incorrect rather than silently dropped.</p>

<h2 id="the-two-requests-that-never-reached-the-model">The Two Requests That Never Reached the Model</h2>

<p>This is the part of the run we did not plan for, and the part most worth documenting. In one early Bedrock <code class="language-plaintext highlighter-rouge">low</code>-effort pass, two of the first ten prompts came back blocked by an automated prompt-safety check before the model produced any output. The prompts were ordinary GSM8K math problems.</p>

<p>The blocks did not arrive as HTTP errors. The stream opened normally and then delivered an error event instead of output, which the harness caught on its stream-error path and recorded in the <code class="language-plaintext highlighter-rouge">error</code> field. The two affected prompts were <code class="language-plaintext highlighter-rouge">gsm8k-test-4</code> and <code class="language-plaintext highlighter-rouge">gsm8k-test-9</code>. This is the real record for the first one, from the preserved partial JSONL (request ID shortened, prompt trimmed):</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"attempts"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"error"</span><span class="p">:</span><span class="w"> </span><span class="s2">"invalid_prompt: This request was blocked by an automated content safety check. If you believe this was made in error, please retry or contact AWS Support with your request ID."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"estimated_grok_cost_usd"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
  </span><span class="nl">"input_tokens"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
  </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"xai.grok-4.3"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ok"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"output_tokens"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
  </span><span class="nl">"prompt_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gsm8k-test-4"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"prompt_text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Solve this grade-school math problem. [...] Problem:</span><span class="se">\n</span><span class="s2">Every day, Wendi feeds each of her chickens three cups [...]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bedrock"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"reasoning_tokens"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
  </span><span class="nl">"request_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"147396ed-..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"run_index"</span><span class="p">:</span><span class="w"> </span><span class="mi">6</span><span class="p">,</span><span class="w">
  </span><span class="nl">"total_ms"</span><span class="p">:</span><span class="w"> </span><span class="mf">3159.35</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ttfb_ms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ttft_ms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Note what the record structure gives you: <code class="language-plaintext highlighter-rouge">ok</code> is false, the token fields are null, TTFB and TTFT are null because no byte of model output ever arrived, and the exact provider message is preserved. We stopped that pass after ten prompts and kept its partial JSONL in <code class="language-plaintext highlighter-rouge">result/bedrock-grok43-low-error-examples</code> as evidence.</p>

<p>The error message itself says to retry, so for the measured runs we set <code class="language-plaintext highlighter-rouge">--retry-policy all-retryable</code> with no attempt cap, and every blocked request eventually went through with exponential backoff. The retries are not hidden: each record carries an <code class="language-plaintext highlighter-rouge">attempts</code> count and a <code class="language-plaintext highlighter-rouge">retry_errors</code> list, so the full history is auditable in the saved artifacts. Across the completed Bedrock runs, the number of requests that hit at least one content-safety block before succeeding was 58 (<code class="language-plaintext highlighter-rouge">none</code>), 581 (<code class="language-plaintext highlighter-rouge">low</code>), 550 (<code class="language-plaintext highlighter-rouge">medium</code>), and 567 (<code class="language-plaintext highlighter-rouge">high</code>), with the worst case needing 10 attempts. The four xAI runs saw zero content-safety blocks; the only retry on that path in all 5,276 requests was a single connection reset. All eight runs finished 1,319 completed, 0 failed. The recorded latency for a retried request is the latency of the final successful attempt, so the retry mechanics inflate wall-clock time, not the per-request distributions.</p>

<p>The engineering takeaway is that the harness treats a blocked request, a timeout, and a wrong answer as three different record types, and the scorer keeps them separate. Accuracy is computed over scoreable answers; reliability is reported alongside it, not blended into it.</p>

<h2 id="running-at-reduced-scale">Running at Reduced Scale</h2>

<p>Before committing to the full 1,319-question run, use <code class="language-plaintext highlighter-rouge">--limit</code> to test against a small slice:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AWS_PROFILE=loka-ml uv run python scripts/benchmark_grok_latency.py \
  --provider bedrock \
  --region us-west-2 \
  --model xai.grok-4.3 \
  --dataset gsm8k \
  --split test \
  --limit 10 \
  --warmups 1 \
  --reasoning-effort none \
  --max-output-tokens 4096 \
  --concurrency 1 \
  --output-dir result/bedrock-grok43-none-pilot
</code></pre></div></div>

<p>This runs the first 10 questions, produces all the same output files, and lets you verify auth, output format, and metric collection before spending the full budget. Our own pilot is saved in <code class="language-plaintext highlighter-rouge">result/bedrock-grok43-none-pilot</code>.</p>

<h2 id="caveats">Caveats</h2>

<p>This benchmark is intentionally narrow:</p>

<ul>
  <li>It measures sequential single-stream requests from one client location. Concurrent-request behavior and multi-region comparisons are out of scope.</li>
  <li>The dataset is grade-school math. Short prompts with compact answers are not representative of all enterprise workloads; longer prompts or outputs would shift the TTFT and throughput numbers. In particular, the flat accuracy curve above <code class="language-plaintext highlighter-rouge">low</code> effort is a property of this workload, not a universal claim about reasoning budgets.</li>
  <li>Latency, tail behavior, and safety-filter behavior are point-in-time properties of a serving fleet. Our numbers reflect the Mantle path as it stood in June 2026, weeks after the Bedrock launch. All of it can change as the fleet matures.</li>
  <li>Cost figures are estimates from list prices as of June 2026. The harness prices both paths at xAI’s published grok-4.3 rates ($1.25 per million input tokens, $2.50 per million output tokens, with reasoning billed as output); if your Bedrock pricing differs, override with <code class="language-plaintext highlighter-rouge">--input-price-per-mtok</code> and <code class="language-plaintext highlighter-rouge">--output-price-per-mtok</code> and the per-record costs recompute.</li>
</ul>

<p>Those caveats do not change the main result: <strong>same model quality through both doors, with the differences showing up in latency shape and reliability behavior rather than accuracy.</strong> But they matter when you translate these numbers into a production SLO.</p>

<h2 id="takeaway">Takeaway</h2>

<p>The useful part is not only the numbers. It is the measurement discipline.</p>

<p>The harness records every request, writes partial results during the run, separates blocked requests from wrong answers, and scores accuracy offline from saved artifacts. That structure is what made an honest comparison possible, including the part we did not expect to find. You can rerun it against your own region, your own workload shape, and your own concurrency level and get numbers that mean something for your system, not just ours.</p>

<p>For teams evaluating Grok 4.3 on Bedrock Mantle against the first-party xAI API, that is the starting point: run the benchmark against your own traffic shape before committing an SLO to results from someone else’s setup.</p>]]></content><author><name>shekswess</name></author><category term="AI" /><category term="LLM" /><category term="xAI" /><category term="Grok 4.3" /><category term="Amazon Bedrock" /><category term="Bedrock Mantle" /><category term="Benchmarking" /><category term="GSM8K" /><category term="AWS" /><summary type="html"><![CDATA[A practical engineering walkthrough for reproducing Loka's Grok 4.3 benchmark across Amazon Bedrock Mantle and the xAI API.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://shekswess.github.io/assets/images/grok-benchmark/background.png" /><media:content medium="image" url="https://shekswess.github.io/assets/images/grok-benchmark/background.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Many Paths to Grok 4.3</title><link href="https://shekswess.github.io/the-many-paths-to-grok-4-3.html" rel="alternate" type="text/html" title="The Many Paths to Grok 4.3" /><published>2026-07-16T22:00:00+00:00</published><updated>2026-07-16T22:00:00+00:00</updated><id>https://shekswess.github.io/the-many-paths-to-grok-4-3</id><content type="html" xml:base="https://shekswess.github.io/the-many-paths-to-grok-4-3.html"><![CDATA[<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />

  <script>
    (function () {
      try { if (localStorage.getItem("loka-theme") === "dark") document.documentElement.setAttribute("data-theme", "dark"); } catch (e) {}
    })();
  </script>

  <meta name="description" content="A public-facing benchmark story on Grok 4.3 through Amazon Bedrock Mantle and the xAI API, focused on accuracy, latency, cost, reliability, and enterprise deployment tradeoffs." />
  <meta property="og:type" content="article" />
  <meta property="og:title" content="The Many Paths to Grok 4.3" />
  <meta property="og:description" content="What changes when Grok 4.3 runs through AWS-native infrastructure: accuracy, latency, cost, reliability, and enterprise deployment tradeoffs." />
  <meta property="og:image" content="/assets/images/grok-benchmark/background.png" />
  <meta property="og:image:alt" content="Grok 4.3 benchmark illustration showing AWS Bedrock Mantle and xAI API paths with accuracy, latency, cost, and reliability signals." />
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="The Many Paths to Grok 4.3" />
  <meta name="twitter:description" content="What changes when Grok 4.3 runs through AWS-native infrastructure: accuracy, latency, cost, reliability, and enterprise deployment tradeoffs." />
  <meta name="twitter:image" content="/assets/images/grok-benchmark/background.png" />
  <title>The Many Paths to Grok 4.3</title>

  <link rel="icon" type="image/svg+xml"
    href="https://cdn.prod.website-files.com/6490383845d4c0f51f929ca8/649052c9d3731fb704eea658_favicon.svg" />

  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  <link rel="stylesheet"
    href="https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap" />

  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
  <style>
    :root {

      --bg: #fcfcfd;

      --bg-soft: #f4f5f7;
      --bg-code: #f2f3f6;
      --fg: #16181f;

      --fg-2: #33353e;
      --fg-3: #61636d;
      --fg-4: #9799a3;
      --line: #e5e5e5;

      --line-strong: #cfd0d7;
      --accent: #1877f2;

      --link: #1877f2;
      --max-content: 680px;

      --max-wide: 900px;

      --max-cover: 960px;

      --toc-width: 200px;
      --toc-gap: 40px;
      --font-sans: "Alliance No.2", "Archivo", -apple-system, "Segoe UI", "Helvetica Neue", Helvetica, sans-serif;
      --font-mono: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", Consolas, monospace;

      --c-blue: #1877f2;

      --c-teal: #0c6f71;

      --c-yellow: #f7d043;

      --c-black: #050517;

      --c-grey: #e5e5e5;

      --c-red: #ef4444;

      --c-purple: #8b5cf6;
      --c-gray: #6b7280;
    }


    :root[data-theme="dark"] {
      --bg: #14161c;
      --bg-soft: #1d2029;
      --bg-code: #1a1c24;
      --fg: #e9eaf1;
      --fg-2: #c3c6d1;
      --fg-3: #969aa8;
      --fg-4: #6c6f7c;
      --line: #2b2e38;
      --line-strong: #3b3e4a;
      --accent: #4f93f7;
      --link: #5b9dff;
    }


    [data-theme="dark"] .status-pill.ok     { color: #86efac; background: #0f2017; border-color: #1f5135; }
    [data-theme="dark"] .status-pill.review { color: #fcd34d; background: #241a07; border-color: #5a4310; }
    [data-theme="dark"] .status-pill.gap    { color: #fca5a5; background: #2a1314; border-color: #5e2526; }

    [data-theme="dark"] .output-box-header.good { color: #86efac; background: #0f2017; border-bottom-color: #1f5135; }
    [data-theme="dark"] .output-box-header.bad  { color: #fca5a5; background: #2a1314; border-bottom-color: #5e2526; }
    [data-theme="dark"] .output-box-header.info { color: #93c5fd; background: #11203a; border-bottom-color: #1e3a64; }


    [data-theme="dark"] .frame      { fill: #1d2029; stroke: #3b3e4a; }
    [data-theme="dark"] .shared     { fill: #15171d; stroke: #3b3e4a; }
    [data-theme="dark"] .accent-box { fill: #12223d; stroke: #4f93f7; }
    [data-theme="dark"] .alt-box    { fill: #0c241f; stroke: #2aa3a5; }
    [data-theme="dark"] .dark       { fill: #050517; stroke: #3b3e4a; }
    [data-theme="dark"] .line       { stroke: #6c6f7c; }
    [data-theme="dark"] svg text    { fill: var(--fg); }


    [data-theme="dark"] .chart-frame { background: #fcfcfd; border-color: #cfd0d7; }


    [data-theme="dark"] .loka-logo { color: #fff; }

    *,
    *::before,
    *::after {
      box-sizing: border-box;
    }

    html {
      scroll-behavior: smooth;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }

    body {
      margin: 0;
      background: var(--bg);
      color: var(--fg);
      font-family: var(--font-sans);
      font-size: 17px;
      line-height: 1.7;
    }

    a {
      color: var(--link);
    }


    .reading-progress {
      position: fixed;
      top: 0;
      left: 0;
      width: 0%;
      height: 2px;
      background: var(--accent);
      z-index: 1000;
      transition: width 0.1s linear;
    }


    .shell {
      min-height: 100vh;
    }

    #main.post {
      max-width: var(--max-wide);
      margin: 0 auto;
      padding: 0 1.6rem 4rem;
      position: relative;
    }


    .post-heading {
      text-align: center;
      max-width: var(--max-content);
      margin: 3.5rem auto 0;
    }

    .post-title {
      margin: 0;
      font-size: 2.4rem;
      line-height: 1.2;
      font-weight: 700;
      letter-spacing: -0.02em;
    }

    .post-title-main {
      display: block;
    }

    .post-title-sub {
      display: block;
      margin-top: 0.35rem;
      font-size: 0.92em;
      font-weight: 600;
      color: var(--fg-2);
    }


    .authors {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      gap: 0.5rem 2rem;
      margin: 1.6rem auto 0;
      max-width: 420px;
      text-align: center;
      font-family: var(--font-sans);
    }

    .author-card {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 0.1rem;
    }

    .author-name {
      font-size: 1rem;
      font-weight: 500;
      color: var(--fg);
    }

    .author-name a {
      color: var(--fg);
      text-decoration: none;
      border-bottom: 1px solid var(--line);
    }

    .author-name a:hover {
      border-bottom-color: var(--fg);
    }

    .author-role {
      font-size: 0.82rem;
      color: var(--fg-2);
    }

    .author-email {
      font-size: 0.82rem;
      color: var(--fg-3);
    }

    .author-email a {
      color: var(--fg-3);
      text-decoration: none;
    }

    .author-email a:hover {
      color: var(--accent);
    }

    .author-affiliation {
      margin: 1rem 0 0;
      text-align: center;
    }

    .author-affiliation .loka-logo {
      height: 2rem;
      vertical-align: middle;
    }

    .post-date {
      margin: 0.8rem 0 0;
      font-family: var(--font-sans);
      font-size: 0.88rem;
      color: var(--fg-4);
    }

    .resource-links {
      margin: 1rem 0 0;
      padding: 0;
      list-style: none;
      display: flex;
      justify-content: center;
      align-items: center;
      gap: 1.5rem;
      font-family: var(--font-sans);
      font-size: 0.88rem;
    }

    .resource-links a {
      color: var(--fg-3);
      text-decoration: none;
      border-bottom: none;
      display: inline-flex;
      align-items: center;
      gap: 0.35rem;
    }

    .resource-links a:hover {
      color: var(--fg);
    }

    .resource-links .icon {
      display: inline-block;
      width: 2rem;
      height: 2rem;
    }


    .resource-links .brand {
      display: block;
      width: auto;
      opacity: 0.82;
      transition: opacity 0.15s ease;
    }

    .resource-links a:hover .brand {
      opacity: 1;
    }

    .brand-openai {
      height: 1.05rem;
    }

    .brand-aws {
      height: 1.5rem;
    }

    .brand-loka {
      height: 1rem;
    }


    .post-cover {
      width: min(var(--max-cover), calc(100vw - 3.2rem));
      max-width: none;
      margin: 2.2rem auto 2.5rem;
      margin-left: 50%;
      transform: translateX(-50%);
    }

    .post-cover img {
      width: 100%;
      height: auto;
      display: block;
      border-radius: 18px;
      border: 1px solid var(--line);
    }


    article.content {
      max-width: var(--max-content);
      margin: 0 auto;
      position: relative;
    }


    .callout {
      border-left: 3px solid var(--accent);
      padding: 0.8rem 1.1rem;
      margin-bottom: 1.3rem;
      background: var(--bg-soft);
    }

    .callout strong {
      font-family: var(--font-sans);
      font-weight: 500;
    }

    .callout p:first-child {
      margin-top: 0;
    }

    .callout p:last-child,
    .callout ul:last-child {
      margin-bottom: 0;
    }

    .callout ul {
      margin-left: 1.2rem;
    }

    .callout li {
      margin-bottom: 0.3rem;
    }

    .callout.warn {
      border-left-color: var(--c-yellow);
    }




    .equation {
      margin: 0.9rem 0 1.1rem;
      padding: 0.65rem 1rem;
      background: var(--bg-soft);
      border: 1px solid var(--line);
      border-radius: 12px;
      text-align: center;
    }

    .equation .equation-inner {
      display: inline-block;
      font-size: 1.05em;
    }


    .figure-wide {
      max-width: var(--max-wide);
      margin-left: calc((var(--max-content) - var(--max-wide)) / 2);
      margin-right: calc((var(--max-content) - var(--max-wide)) / 2);
    }

    .figure-wide canvas {
      width: 100% !important;
      height: auto !important;
    }

    .chart-frame {
      border: 1px solid var(--line);
      border-radius: 12px;
      padding: 1rem 1.1rem 0.6rem;
      background: var(--bg);
    }


    .video-figure video {
      width: 100%;
      height: auto;
      display: block;
      border-radius: 14px;
      border: 1px solid var(--line);
      background: #000;
    }


    .left-toc {
      position: sticky;
      top: 2rem;
      float: left;
      margin-left: calc(-1 * var(--toc-width) - var(--toc-gap));
      width: var(--toc-width);
      font-size: 0.85rem;
      line-height: 1.45;
      max-height: calc(100vh - 4rem);
      overflow-y: auto;
    }

    .toc-title {
      margin: 0 0 0.5rem;
      font-family: var(--font-sans);
      font-size: 0.7rem;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--fg-4);
      font-weight: 500;
    }

    .toc-list {
      margin: 0;
      padding: 0;
      list-style: none;
      border-left: 1px solid var(--line);
    }

    .toc-list li {
      margin: 0;
    }

    .toc-list a {
      display: block;
      padding: 0.2rem 0 0.2rem 0.85rem;
      text-decoration: none;
      color: var(--fg-4);
      border-left: 2px solid transparent;
      margin-left: -1px;
      transition: color 0.15s ease, border-color 0.15s ease;
    }

    .toc-list a:hover {
      color: var(--fg-2);
    }

    .toc-list a.active {
      color: var(--accent);
      border-left-color: var(--accent);
      font-weight: 500;
    }


    .content p,
    .content ul,
    .content ol,
    .content pre,
    .content blockquote,
    .content table,
    .content figure {
      margin: 0 0 1.3rem;
    }


    .content h2,
    .content h3,
    .content h4,
    .content h5,
    .content h6 {
      line-height: 1.3;
      position: relative;
    }

    .content h2 {
      margin: 2.8rem 0 0.9rem;
      font-size: 1.6rem;
      font-weight: 700;
      letter-spacing: -0.01em;
    }

    .content h3 {
      margin: 2rem 0 0.7rem;
      font-size: 1.25rem;
      font-weight: 600;
    }

    .content h4 {
      margin: 1.6rem 0 0.5rem;
      font-size: 1.1rem;
      font-weight: 600;
      color: var(--fg-2);
    }

    .content h5 {
      margin: 1.4rem 0 0.4rem;
      font-size: 0.8rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.08em;
      color: var(--fg-3);
    }

    .content h6 {
      margin: 1.2rem 0 0.3rem;
      font-size: 0.8rem;
      font-weight: 500;
      text-transform: uppercase;
      letter-spacing: 0.08em;
      color: var(--fg-4);
    }

    .heading-anchor {
      position: absolute;
      left: -1.2rem;
      top: 50%;
      transform: translateY(-50%);
      opacity: 0;
      text-decoration: none;
      color: var(--fg-4);
      font-weight: 400;
      font-size: 0.85em;
      transition: opacity 0.15s ease;
    }

    .content h2:hover .heading-anchor,
    .content h3:hover .heading-anchor,
    .content h4:hover .heading-anchor,
    .content h5:hover .heading-anchor,
    .content h6:hover .heading-anchor,
    .heading-anchor:focus {
      opacity: 1;
    }

    .content ul {
      margin-left: 1.5rem;
      padding: 0;
      list-style: disc;
    }

    .content ol {
      margin-left: 1.7rem;
    }

    .content li {
      margin-bottom: 0.25rem;
    }

    .content a {
      text-decoration-color: var(--line-strong);
      text-underline-offset: 2px;
      transition: text-decoration-color 0.15s ease;
    }

    .content a:hover {
      text-decoration-color: var(--fg);
    }

    .content blockquote {
      margin-left: 0;
      padding: 0.5rem 1.1rem;
      border-left: 3px solid var(--line-strong);
      color: var(--fg-2);
    }

    .content code {
      font-family: var(--font-mono);
      font-size: 0.84em;
      background: var(--bg-code);
      padding: 0.1rem 0.3rem;
      border-radius: 3px;
    }

    .content pre {
      background: var(--bg-code);
      border-radius: 4px;
      overflow-x: auto;
      padding: 0.9rem 1rem;
      border: 1px solid var(--line);
    }

    .content pre code {
      padding: 0;
      background: none;
      display: block;
      font-size: 0.8rem;
      line-height: 1.5;
      border-radius: 0;
    }


    .content table {
      width: 100%;
      border-collapse: collapse;
      font-size: 0.9rem;
    }

    .content th,
    .content td {
      padding: 0.5rem 0.65rem;
      text-align: left;
      vertical-align: top;
      border-bottom: 1px solid var(--line);
    }

    .content th {
      font-family: var(--font-sans);
      font-weight: 500;
      font-size: 0.82rem;
      color: var(--fg-3);
      border-bottom: 2px solid var(--line-strong);
    }

    .content tbody tr:last-child td {
      border-bottom: none;
    }

    .content td code {
      font-size: 0.8rem;
    }


    .status-pill {
      font-family: var(--font-sans);
      font-size: 0.72rem;
      font-weight: 500;
      padding: 0.08rem 0.45rem;
      border-radius: 999px;
      white-space: nowrap;
    }

    .status-pill.ok {
      color: #166534;
      background: #f0fdf4;
      border: 1px solid #bbf7d0;
    }

    .status-pill.review {
      color: #92400e;
      background: #fffbeb;
      border: 1px solid #fde68a;
    }

    .status-pill.gap {
      color: #991b1b;
      background: #fef2f2;
      border: 1px solid #fecaca;
    }


    .content figure {
      margin-top: 1.6rem;
    }

    .content figure img,
    .content figure svg {
      display: block;
      width: 100%;
      height: auto;
    }

    .content figcaption {
      margin-top: 0.5rem;
      font-size: 0.88rem;
      color: var(--fg-3);
      font-style: italic;
      text-align: center;
    }


    .citation-block {
      margin-top: 0.5rem;
    }

    .citation-block pre {
      background: var(--bg-soft);
      border: 1px solid var(--line);
      border-radius: 4px;
      padding: 0.8rem 1rem;
      font-size: 0.78rem;
      line-height: 1.5;
      overflow-x: auto;
    }

    .citation-block pre code {
      font-family: var(--font-mono);
      background: none;
      padding: 0;
      color: var(--fg-2);
    }


    .references-list {
      margin-left: 1.4rem;
      padding: 0;
      list-style: decimal;
    }

    .references-list li {
      margin-bottom: 1rem;
      font-size: 0.9rem;
      line-height: 1.65;
      color: var(--fg-2);
    }

    .references-list li a {
      display: inline-block;
      margin-top: 0.1rem;
      font-family: var(--font-sans);
      font-size: 0.82rem;
      color: var(--fg-3);
    }


    .paginator {
      max-width: var(--max-content);
      margin: 2.5rem auto 0;
      padding-top: 1.2rem;
      border-top: 1px solid var(--line);
      display: flex;
      justify-content: center;
      gap: 2rem;
      font-family: var(--font-sans);
      font-size: 0.88rem;
    }

    .paginator a {
      text-decoration: none;
      color: var(--fg-3);
      transition: color 0.15s ease;
    }

    .paginator a:hover {
      color: var(--fg);
    }


    .back-to-top {
      position: fixed;
      bottom: 2rem;
      right: 2rem;
      width: 36px;
      height: 36px;
      border-radius: 50%;
      background: var(--bg);
      color: var(--fg-3);
      border: 1px solid var(--line);
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      opacity: 0;
      transform: translateY(8px);
      transition: opacity 0.25s ease, transform 0.25s ease, color 0.15s ease, border-color 0.15s ease;
      z-index: 100;
      font-size: 16px;
      line-height: 1;
    }

    .back-to-top.visible {
      opacity: 1;
      transform: translateY(0);
    }

    .back-to-top:hover {
      color: var(--accent);
      border-color: var(--accent);
    }


    .theme-toggle {
      position: fixed;
      top: 1rem;
      right: 1rem;
      width: 36px;
      height: 36px;
      border-radius: 50%;
      background: var(--bg);
      color: var(--fg-3);
      border: 1px solid var(--line);
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: color 0.15s ease, border-color 0.15s ease;
      z-index: 200;
    }

    .theme-toggle:hover {
      color: var(--accent);
      border-color: var(--accent);
    }

    .theme-toggle svg {
      width: 18px;
      height: 18px;
    }


    .theme-toggle .icon-sun {
      display: none;
    }

    [data-theme="dark"] .theme-toggle .icon-moon {
      display: none;
    }

    [data-theme="dark"] .theme-toggle .icon-sun {
      display: block;
    }


    .loka-logo {
      display: inline-block;
      height: 1.15em;
      width: auto;
      vertical-align: -0.15em;
      margin-right: 0.15em;
      color: var(--c-blue);
    }

    .site-footer .loka-logo {
      height: 1em;
      vertical-align: -0.1em;
    }



    .site-footer {
      max-width: var(--max-content);
      margin: 0 auto;
      padding: 1.5rem 1.6rem 2rem;
      font-family: var(--font-sans);
      font-size: 0.82rem;
      color: var(--fg-4);
      text-align: center;
    }

    .site-footer a {
      color: var(--fg-3);
      text-decoration: none;
    }

    .site-footer a:hover {
      color: var(--fg);
    }


    .output-comparison {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 1rem;
      margin: 1.3rem 0;
    }

    .output-box {
      border: 1px solid var(--line);
      border-radius: 6px;
      overflow: hidden;
    }

    .output-box-header {
      font-family: var(--font-sans);
      font-size: 0.78rem;
      font-weight: 500;
      padding: 0.5rem 0.8rem;
      border-bottom: 1px solid var(--line);
      background: var(--bg-soft);
    }

    .output-box-header.good {
      color: #166534;
      background: #f0fdf4;
      border-bottom-color: #bbf7d0;
    }

    .output-box-header.bad {
      color: #991b1b;
      background: #fef2f2;
      border-bottom-color: #fecaca;
    }

    .output-box-header.info {
      color: #1e40af;
      background: #eff6ff;
      border-bottom-color: #bfdbfe;
    }

    .output-box-body {
      padding: 0.8rem;
      font-size: 0.82rem;
      line-height: 1.55;
      max-height: 300px;
      overflow-y: auto;
    }

    .output-box-body code {
      font-size: 0.78rem;
    }


    .frame {
      fill: #fff;
      stroke: #d0d0d0;
      stroke-width: 1.5;
    }

    .shared {
      fill: #f7f7f7;
      stroke: #d0d0d0;
      stroke-width: 1.5;
    }

    .accent-box {
      fill: #eff6ff;
      stroke: #1877f2;
      stroke-width: 1.5;
    }


    .alt-box {
      fill: #f0fdf4;
      stroke: #0c6f71;
      stroke-width: 1.5;
    }


    .dark {
      fill: #050517;
      stroke: #050517;
    }

    .line {
      stroke: #999;
      stroke-width: 1.5;
      fill: none;
    }

    .label {
      font-weight: 500;
    }


    @media (max-width: 1100px) {
      .left-toc {
        display: none;
      }

      .heading-anchor {
        display: none;
      }

      .figure-wide {
        max-width: var(--max-content);
        margin-left: 0;
        margin-right: 0;
      }
    }

    @media (max-width: 768px) {
      body {
        font-size: 16px;
      }

      #main.post {
        padding: 0 1.1rem 3rem;
      }

      .post-title {
        font-size: 1.7rem;
      }

      .resource-links {
        gap: 1rem;
        font-size: 0.82rem;
      }

      .content h2 {
        font-size: 1.3rem;
      }

      .callout {
        padding: 0.7rem 0.9rem;
      }

      .content table {
        font-size: 0.8rem;
      }

      .content th,
      .content td {
        padding: 0.4rem 0.45rem;
      }

      .back-to-top {
        bottom: 1.2rem;
        right: 1.2rem;
      }

      .output-comparison {
        grid-template-columns: 1fr;
      }

      .authors {
        flex-direction: column;
        align-items: center;
      }
    }

    @media print {

      .reading-progress,
      .left-toc,
      .back-to-top,
      .theme-toggle,
      .paginator {
        display: none !important;
      }

      body {
        font-size: 11pt;
      }

      .figure-wide {
        max-width: 100%;
        margin-left: 0;
        margin-right: 0;
      }

      a {
        color: inherit;
      }
    }
  </style>


  <!-- Umami analytics placeholder — KEEP this comment in the produced page (do NOT strip it).
       Added manually after posting: uncomment and set the real website id (and script URL if self-hosted).
       <script defer src="https://cloud.umami.is/script.js" data-website-id="UMAMI_WEBSITE_ID"></script>
  -->
</head>

<body>

  <svg width="0" height="0" style="position: absolute" aria-hidden="true" focusable="false">
    <symbol id="loka-wordmark" viewBox="0 0 102 33">
      <path d="M99.9188 22.3945C100.278 22.396 100.632 22.4881 100.946 22.6624C101.271 22.8377 101.539 23.1027 101.718 23.4258C101.902 23.7426 101.999 24.1018 102 24.4678C102.003 24.8299 101.91 25.1864 101.731 25.5017C101.553 25.8243 101.286 26.0899 100.962 26.2678C100.647 26.4468 100.29 26.5409 99.9268 26.5409C99.5638 26.5409 99.2071 26.4468 98.8916 26.2678C98.5684 26.0891 98.302 25.8237 98.1226 25.5017C97.9437 25.1865 97.8509 24.8299 97.8537 24.4678C97.8505 24.103 97.9432 23.7438 98.1226 23.4258C98.3026 23.1031 98.5712 22.8383 98.897 22.6624C99.2095 22.4887 99.561 22.3966 99.9188 22.3945V22.3945ZM99.9188 22.8017C99.6308 22.8028 99.3477 22.8766 99.096 23.016C98.8343 23.157 98.6187 23.3699 98.4749 23.6294C98.3276 23.8845 98.2497 24.1735 98.249 24.4678C98.2485 24.759 98.3245 25.0453 98.4695 25.2982C98.6137 25.5578 98.8279 25.7722 99.0879 25.9169C99.3412 26.0604 99.6274 26.1361 99.9188 26.1366C100.21 26.1375 100.497 26.0617 100.75 25.9169C101.011 25.773 101.226 25.5585 101.371 25.2982C101.516 25.0453 101.592 24.759 101.591 24.4678C101.591 24.1735 101.513 23.8845 101.365 23.6294C101.222 23.3686 101.005 23.1553 100.742 23.016C100.491 22.8764 100.209 22.8027 99.9215 22.8017H99.9188ZM98.9777 25.5767V23.3776H99.4267C99.8462 23.3776 100.08 23.3776 100.131 23.3776C100.251 23.3833 100.368 23.4133 100.475 23.466C100.563 23.5158 100.635 23.5891 100.682 23.6776C100.739 23.7724 100.768 23.8809 100.766 23.991C100.769 24.1426 100.713 24.2896 100.61 24.4008C100.493 24.5195 100.338 24.5934 100.172 24.6098C100.228 24.629 100.28 24.657 100.327 24.6928C100.383 24.7437 100.432 24.8003 100.475 24.8615C100.492 24.883 100.548 24.9714 100.639 25.1294L100.908 25.5714H100.354L100.169 25.2151C100.095 25.0461 99.9905 24.8925 99.8596 24.7624C99.7699 24.6978 99.6611 24.6648 99.5504 24.6687H99.4348V25.5714L98.9777 25.5767ZM99.4267 24.3285H99.6069C99.768 24.3367 99.9296 24.3259 100.088 24.2964C100.148 24.2797 100.201 24.244 100.239 24.1946C100.274 24.1451 100.292 24.0862 100.293 24.0258C100.293 23.9669 100.274 23.9095 100.239 23.8624C100.202 23.8121 100.149 23.7761 100.088 23.7606C99.9299 23.7285 99.7681 23.7168 99.6069 23.7258H99.4267V24.3285Z" fill="currentColor"/>
<path d="M0 5.61719H4.37561V22.5767H13.4185V26.9628H0V5.61719Z" fill="currentColor"/>
<path d="M51.9241 5.61719H56.5914V16.7286L68.0263 5.61719H73.8021L63.534 15.8221L63.2715 16.056L70.885 23.6878L88.9125 5.61719H93.0548V26.6704H88.6792V11.7577L74.0355 26.6704H67.968L60.7336 19.331L60.3836 19.0093L56.5914 22.7229V26.9628H51.9241V5.61719Z" fill="currentColor"/>
<path d="M32.2342 0C23.3954 0 16.2486 7.16392 16.2486 16.0238C16.2486 24.8837 23.3954 32.0476 32.2342 32.0476C41.0729 32.0476 48.2197 24.8544 48.2197 16.0238C48.2197 7.16392 41.0729 0 32.2342 0ZM32.1175 27.6323C25.6999 27.6323 20.5367 22.4275 20.5367 16.0238C20.5367 9.59089 25.7291 4.41532 32.1175 4.41532C38.535 4.41532 43.6982 9.62013 43.6982 16.0238C43.6982 22.4275 38.5059 27.6323 32.1175 27.6323Z" fill="currentColor"/>
<path d="M35.7338 16.0285C35.7338 17.9291 34.1878 19.4789 32.2917 19.4789C30.3956 19.4789 28.8495 17.9291 28.8495 16.0285C28.8495 14.1279 30.3956 12.5781 32.2917 12.5781C34.1878 12.5781 35.7338 14.1279 35.7338 16.0285Z" fill="currentColor"/>
    </symbol>
  </svg>
  <div class="reading-progress" id="reading-progress"></div>
  <a id="top"></a>
  <div class="shell">
    <main id="main" class="post">

      <section class="post-heading">
        <h1 class="post-title">
          <span class="post-title-main">The Many Paths to Grok 4.3</span>
          <span class="post-title-sub">What changes when xAI's reasoning-first model runs through AWS-native infrastructure</span>
        </h1>
        <div class="authors">
          <div class="author-card">
            <span class="author-name">Nina Cvetkovska</span>
            <span class="author-role">Machine Learning Engineer</span>
            <span class="author-email"><a href="mailto:nina.cvetkovska@loka.com">nina.cvetkovska@loka.com</a></span>
          </div>
          <div class="author-card">
            <span class="author-name">Bojan Jakimovski</span>
            <span class="author-role">Machine Learning Engineering Lead</span>
            <span class="author-email"><a href="mailto:bojan.jakimovski@loka.com">bojan.jakimovski@loka.com</a></span>
          </div>
        </div>
        <div class="author-affiliation">
          <a href="https://www.loka.com/" aria-label="Loka">
            <svg class="loka-logo" viewBox="0 0 102 33" aria-hidden="true"><use href="#loka-wordmark" /></svg>
          </a>
        </div>
        <p class="post-date"><time datetime="2026-07">July 2026</time></p>
        <p class="post-date"><a href="/grok-4-3-engineering-walkthrough.html">Read the engineering walkthrough →</a></p>
      </section>

      <figure class="post-cover">
        <img src="/assets/images/grok-benchmark/background.png" alt="Grok 4.3 benchmark illustration showing AWS Bedrock Mantle and xAI API paths with accuracy, latency, cost, and reliability signals." />
      </figure>

      <article class="content"><nav aria-label="Table of contents" class="left-toc"><p class="toc-title">Contents</p><ul class="toc-list"><li><a href="#opening">Why this matters</a></li><li><a href="#what-we-wanted-to-learn">What we wanted to learn</a></li><li><a href="#what-we-tested">What we tested</a></li><li><a href="#results">What we got</a></li><li><a href="#latency-cost-tradeoff">Reading the tradeoff</a></li><li><a href="#reliability">Reliability behavior</a></li><li><a href="#bigger-picture">The bigger picture</a></li><li><a href="#enterprise-takeaways">Takeaways</a></li><li><a href="#conclusion">Conclusion</a></li><li><a href="#citation">Citation</a></li><li><a href="#references">Further reading</a></li></ul></nav><h2 id="opening">
                  Why this matters
                  <a aria-label="Link to section" class="heading-anchor" href="#opening">#</a>
                </h2><p>In June 2026, <a href="https://aws.amazon.com/about-aws/whats-new/2026/06/grok-amazon-bedrock/" target="_blank" rel="noopener">xAI joined Amazon Bedrock as a model provider</a>, and Grok 4.3 became the first model to run on <a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-xai-grok-4-3.html" target="_blank" rel="noopener">Mantle, Bedrock's new OpenAI-compatible inference engine</a>. For teams that already live inside AWS, that's a bigger deal than it sounds: it means access to xAI's reasoning-first model without a new vendor relationship, a new billing pipeline, or a security review that starts from zero.</p><p>But availability is only half the story. The question we kept hearing from clients wasn't whether Grok 4.3 is good; <a href="https://x.ai/news/grok-amazon-bedrock" target="_blank" rel="noopener">xAI's own benchmarks</a> make that case. The question was more practical: <em>does anything change when the same model runs through a different door?</em> Does the AWS-native path affect accuracy? Latency? Does it fail differently under load? Nobody had published a straight answer, so we ran the experiment ourselves.</p><p>This isn't a leaderboard post, and it isn't an implementation walkthrough. We took the same workload, the same evaluation, and pointed it at both routes to Grok 4.3, Amazon Bedrock Mantle and the first-party xAI API, and measured what actually differed.</p><div class="callout"><p>The short version: once reasoning was enabled, final accuracy was nearly identical on both paths. Where the paths genuinely diverged was latency, reliability behavior, and the operational machinery around the model. In our experience, that is exactly where enterprise deployments succeed or stall.</p></div><h2 id="what-we-wanted-to-learn">
                  What we wanted to learn
                  <a aria-label="Link to section" class="heading-anchor" href="#what-we-wanted-to-learn">#</a>
                </h2><p>If your organization has standardized on AWS (procurement, security review, IAM, regional architecture, cost allocation), then Bedrock Mantle is the path of least resistance to Grok 4.3. The question is whether "least resistance" comes with tradeoffs you'd want to know about before committing.</p><p>So we framed the benchmark around the questions a technical decision-maker would ask before recommending one path over the other:</p><ul><li>Does Grok 4.3 deliver the same final-answer quality through Bedrock Mantle as through the xAI API?</li><li>How much latency does each step of reasoning effort really add?</li><li>Where does cost start to climb, and does accuracy keep pace with it?</li><li>Do the two paths fail in different ways?</li><li>Which reasoning-effort setting is the sensible default?</li></ul><p>A quick note on scope: Mantle itself deserves more than a passing mention, and we give it one <a href="#bigger-picture">later in the post</a>. For now, the relevant point is that it puts Grok 4.3 behind AWS billing, governance, and regional deployment patterns your organization has likely already approved.</p><h2 id="what-we-tested">
                  What we tested
                  <a aria-label="Link to section" class="heading-anchor" href="#what-we-tested">#</a>
                </h2><p>We ran Grok 4.3 against the full <a href="https://arxiv.org/abs/2110.14168" target="_blank" rel="noopener">GSM8K</a> test split: <strong>1,319</strong> grade-school math word problems. GSM8K is a narrow benchmark, and we chose it deliberately for that reason. Because the task is so well-defined (read a short problem, produce one correct number), it gives us a clean, controlled way to compare final-answer accuracy, reasoning behavior, latency, and cost across the two paths without application-specific prompt design muddying the signal. (The dataset is <a href="https://github.com/openai/grade-school-math" target="_blank" rel="noopener">openly available</a> if you want to reproduce the setup.)</p><p>Both paths got identical treatment: the same prompt shape, streaming enabled, one request at a time, and all four reasoning-effort settings that Grok 4.3 exposes (<code>none</code>, <code>low</code>, <code>medium</code>, and <code>high</code>). For each run we measured final-answer accuracy, p50 and p95 end-to-end latency, time to first visible token, median reasoning-token usage, and estimated run cost.</p><p>Scoring was deliberately unforgiving: the final numeric answer had to match the reference exactly. A beautifully reasoned explanation ending in the wrong number scored zero. Strict scoring keeps the comparison honest and easy to interpret.</p><h2 id="results">
                  What we got
                  <a aria-label="Link to section" class="heading-anchor" href="#results">#</a>
                </h2><p>The most useful finding is also the simplest: <strong>reasoning effort mattered far more than provider path</strong> for final accuracy. With reasoning turned on at any level, both paths landed in the same <strong>96–97%</strong> band. Whichever door you walk through, you get essentially the same model.</p><p>Turning reasoning off entirely was the fastest and cheapest configuration, and also the least accurate. The single biggest jump in the whole benchmark came from switching to <code>low</code> effort. On Bedrock, accuracy climbed from <strong>93.10%</strong> to <strong>96.51%</strong>. On the xAI API, from <strong>94.16%</strong> to <strong>96.89%</strong>.</p><figure class="figure-wide"><div class="chart-frame"><canvas height="210" id="chart-accuracy"></canvas></div><figcaption>Figure 1. GSM8K accuracy by provider and reasoning effort. Low effort captured most of the accuracy gain.</figcaption></figure><p>After that, the curve went flat. <code>medium</code> and <code>high</code> bought essentially nothing on GSM8K: Bedrock reached <strong>96.59%</strong> at medium and actually dipped to <strong>96.13%</strong> at high, while xAI reached <strong>96.97%</strong> at medium and <strong>96.89%</strong> at high. Those differences are noise, not signal.</p><p>The clearest way to see it is to plot each step up in reasoning effort against the extra median latency it costs. The move from <code>none</code> to <code>low</code> sits in the part of the chart you want to be in: a large accuracy gain for a few additional seconds. Every step after that clusters near zero accuracy gain while latency keeps climbing.</p><figure class="figure-wide"><div class="chart-frame"><canvas height="230" id="chart-gain-latency"></canvas></div><figcaption>Figure 2. Accuracy gain versus added median latency for each reasoning-effort step. The first step to low effort delivered the meaningful payoff.</figcaption></figure><p>To be clear, this doesn't mean higher effort is never worth it; harder workloads may well need the extra thinking budget. It means that for structured arithmetic like GSM8K, the model had all the room it needed at <code>low</code>. Beyond that point, you're mostly buying tokens, not accuracy.</p><h2 id="latency-cost-tradeoff">
                  Reading the tradeoff
                  <a aria-label="Link to section" class="heading-anchor" href="#latency-cost-tradeoff">#</a>
                </h2><p>Latency is where the two paths stopped looking identical. Enabling <code>low</code> effort raised median latency on both: Bedrock moved from <strong>0.79s</strong> to <strong>3.56s</strong>, and xAI from <strong>1.05s</strong> to <strong>2.81s</strong>. That is the expected price of reasoning. The interesting part is the tail.</p><p>Anyone who has shipped a user-facing product knows the tail is what users remember. A workflow can feel perfectly fine at the median and still feel broken when p95 spikes. In this run, the xAI API held a meaningfully tighter tail at low effort: <strong>5.45s</strong> at p95 versus <strong>9.70s</strong> on Bedrock.</p><figure class="figure-wide"><div class="chart-frame"><canvas height="210" id="chart-latency"></canvas></div><figcaption>Figure 3. End-to-end p50 and p95 latency. The provider path showed up more clearly in latency than in final accuracy.</figcaption></figure><p>The token story told the same tale from another angle. <code>low</code> effort used roughly <strong>260–290</strong> median reasoning tokens per question. <code>medium</code> and <code>high</code> pushed that past <strong>400</strong> on both paths while, as we saw above, accuracy barely moved.</p><figure class="figure-wide"><div class="chart-frame"><canvas height="210" id="chart-reasoning"></canvas></div><figcaption>Figure 4. Median reasoning tokens by effort. Higher effort increased reasoning-token usage more clearly than it improved GSM8K accuracy.</figcaption></figure><p>Cost tracked the reasoning tokens almost exactly, which makes the recommendation straightforward: <code>low</code> effort was the sweet spot in this benchmark. It captured most of the quality improvement without paying the full latency and token bill of medium or high.</p><h3>Full benchmark view</h3><p>Here's everything in one place: accuracy, latency, throughput, reasoning tokens, and cost for all eight runs.</p><table><thead><tr><th>Run</th><th>Provider</th><th>Effort</th><th>Accuracy</th><th>p50</th><th>p95</th><th>TTFT p50</th><th>Tok/s p50</th><th>Reason p50</th><th>Cost</th></tr></thead><tbody>
      <tr><td><code>bedrock-grok43-none</code></td><td>Bedrock</td><td>none</td><td>93.10%</td><td>0.79</td><td>1.72</td><td>0.62</td><td>88.3</td><td>0</td><td>$0.4418</td></tr>
      <tr><td><code>bedrock-grok43-low</code></td><td>Bedrock</td><td>low</td><td>96.51%</td><td>3.56</td><td>9.70</td><td>3.14</td><td>107.6</td><td>289</td><td>$1.5347</td></tr>
      <tr><td><code>bedrock-grok43-medium</code></td><td>Bedrock</td><td>medium</td><td>96.59%</td><td>3.81</td><td>11.24</td><td>3.46</td><td>159.0</td><td>467</td><td>$2.4169</td></tr>
      <tr><td><code>bedrock-grok43-high</code></td><td>Bedrock</td><td>high</td><td>96.13%</td><td>3.61</td><td>10.41</td><td>3.32</td><td>165.8</td><td>459</td><td>$2.4400</td></tr>
      <tr><td><code>xai-grok43-none</code></td><td>xAI</td><td>none</td><td>94.16%</td><td>1.05</td><td>1.56</td><td>0.67</td><td>54.8</td><td>0</td><td>$0.5883</td></tr>
      <tr><td><code>xai-grok43-low</code></td><td>xAI</td><td>low</td><td>96.89%</td><td>2.81</td><td>5.45</td><td>0.71</td><td>111.1</td><td>264</td><td>$1.5323</td></tr>
      <tr><td><code>xai-grok43-medium</code></td><td>xAI</td><td>medium</td><td>96.97%</td><td>3.87</td><td>9.66</td><td>0.71</td><td>116.3</td><td>399</td><td>$2.1449</td></tr>
      <tr><td><code>xai-grok43-high</code></td><td>xAI</td><td>high</td><td>96.89%</td><td>3.78</td><td>9.96</td><td>0.67</td><td>118.2</td><td>412</td><td>$2.2431</td></tr>
      </tbody></table><p><em>p50 and p95 are measured in seconds. Reasoning tokens are medians. Cost is the estimated cost for the full 1,319-question run.</em></p><h2 id="reliability">
                  Reliability behavior
                  <a aria-label="Link to section" class="heading-anchor" href="#reliability">#</a>
                </h2><p>Here's the part that never shows up on a leaderboard. In one early Bedrock low-effort pass, two of the first ten prompts were stopped by an automated prompt-safety check before the model produced any answer at all. The prompts were ordinary GSM8K math problems, questions about apples and train schedules. That's not a model-quality issue. It's a property of the path the request travels through.</p><p>And that distinction matters enormously for production planning. A blocked request, a timeout, and a wrong answer are three different failure modes with three different fixes, and collapsing them into one accuracy score hides the information you need most. Accuracy tells you whether the model solved the task. Reliability tells you whether the path consistently delivered a scoreable answer in the first place.</p><div class="callout warn"><p>This is why we always recommend benchmarking through your actual deployment path, not a proxy for it. Governance layers, safety filters, streaming behavior, and regional infrastructure all shape the user experience, even when the model underneath is identical.</p></div><h2 id="bigger-picture">
                  The bigger picture: why this pairing matters
                  <a aria-label="Link to section" class="heading-anchor" href="#bigger-picture">#</a>
                </h2><p>Before the takeaways, it's worth zooming out. We've spent this post measuring one workload through two doors, and being picky about tail latencies and safety filters along the way. None of that picking should obscure the larger point: both sides of this partnership shipped something genuinely useful.</p><h3>What AWS built with Mantle</h3><p>Mantle is not a wrapper around a third-party API. It is a <a href="https://aws.amazon.com/about-aws/whats-new/2026/06/grok-amazon-bedrock/" target="_blank" rel="noopener">new inference engine inside Amazon Bedrock</a>, designed for price performance and shipped with the full production feature set from day one: response streaming, structured outputs, tool calling, and Standard and Priority service tiers.</p><p>The design choice that matters most for adoption speed is API compatibility. Mantle speaks the OpenAI API specification, so a team already built on OpenAI SDKs can point <code>base_url</code> at the <a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-xai-grok-4-3.html" target="_blank" rel="noopener">Mantle endpoint</a>, set the model to <code>xai.grok-4.3</code>, and be running inside Bedrock with minimal code changes. Meanwhile everything an enterprise actually worries about (billing, IAM, governance, procurement, regional controls) stays on rails the organization has already approved.</p><div class="callout"><p>As an AWS partner, we read Mantle as infrastructure, not a one-off integration. AWS built an OpenAI-compatible runtime into Bedrock, which positions the platform to onboard future third-party models the same way. Model choice without vendor sprawl is the whole promise of Bedrock, and Mantle extends it.</p></div><h3>What xAI brings with Grok 4.3</h3><p>On the model side, Grok 4.3 earned its place in the catalog. It is a reasoning-first design: reasoning is always active rather than optional, with effort configurable from <code>none</code> to <code>high</code>, which is exactly the lever our benchmark shows paying off. It ships a <a href="https://x.ai/news/grok-amazon-bedrock" target="_blank" rel="noopener">1-million-token context window</a>, and xAI reports top marks on independent evaluations, including the lowest hallucination rate among frontier models on Artificial Analysis's Omniscience benchmark and leading results on real-world tool-calling tests.</p><p>That profile maps cleanly onto the workloads enterprises are trying to ship right now: contract review, case law research, financial document Q&amp;A, and customer-support agents that need reliable tool calling over long context. Combined with Bedrock's on-demand pricing for the model, it makes Grok 4.3 one of the most cost-effective frontier reasoning options on AWS today.</p><p>Our results add the practical footnote: the combination holds up under measurement. Same model quality through the AWS route, with the operational conveniences that come with it.</p><h2 id="enterprise-takeaways">
                  Takeaways for decision-makers
                  <a aria-label="Link to section" class="heading-anchor" href="#enterprise-takeaways">#</a>
                </h2><p>If you're a CTO, Head of AI, or solutions architect weighing this decision, the framing that helps most is this: you're not choosing between two models. You're choosing between two operational environments around the same model. Here's what our data says about that choice:</p><ul><li><strong>The strategic value of Bedrock Mantle is AWS-native access.</strong> Grok 4.3 arrives inside your existing billing, governance, IAM, and deployment patterns, with no new vendor onboarding and no parallel operational stack.</li><li><strong>Model quality is effectively path-independent.</strong> With reasoning enabled, both paths landed in the same 96–97% accuracy band on GSM8K. You're not sacrificing capability for convenience.</li><li><strong><code>low</code> reasoning effort was the best default in this benchmark.</strong> It delivered nearly all of the accuracy lift at a fraction of the token and latency cost of medium or high.</li><li><strong>Medium and high effort need to earn their keep.</strong> On this workload they increased reasoning tokens and cost far more than they improved accuracy. Justify them with your own workload data, not intuition.</li><li><strong>The paths diverge in latency shape and reliability behavior, not accuracy.</strong> Tail latency (p95) and pre-model safety filtering are where the differences live, and where your production architecture review should focus.</li></ul><h2 id="conclusion">
                  Conclusion
                  <a aria-label="Link to section" class="heading-anchor" href="#conclusion">#</a>
                </h2><p>Grok 4.3 landing on Bedrock Mantle changes the adoption conversation for AWS-centered organizations. Governance, procurement, regional planning, streaming, structured outputs, and tool calling stop being separate integration projects and become part of the same motion as adopting the model itself. That's the real headline; the benchmark just tells you what it costs.</p><p>And what it costs turns out to be encouragingly little. Our practical recommendation from 10,552 scored requests: start with <code>low</code> reasoning effort for structured, GSM8K-like workloads. It captured most of the accuracy gain while keeping latency and cost under control. Reach for <code>medium</code> or <code>high</code> only when your own evaluation proves the extra reasoning budget pays for itself, because on this workload it didn't.</p><p>Finally, evaluate the path for what it actually changes: latency shape, tail behavior, reliability under safety filtering, governance fit, and day-two operations. When final accuracy is this close, the right decision stops being about the model alone and starts being about the system you wrap around it.</p><p><em>If you're evaluating Grok 4.3, or any foundation model, for a production workload on AWS, this is the kind of benchmarking <a href="https://www.loka.com/">Loka's AI engineering team</a> runs for clients every week. We're happy to compare notes.</em></p><h2 id="citation">
                  Citation
                  <a aria-label="Link to section" class="heading-anchor" href="#citation">#</a>
                </h2><p>If you find this work useful, please cite:</p><div class="citation-block"><pre><code>@misc{loka2026grok43bedrock,
  title        = {The Many Paths to Grok 4.3},
  author       = {Cvetkovska, Nina and Jakimovski, Bojan},
  year         = {2026},
  month        = {jul},
  howpublished = {Blog post},
  url          = {https://lokahq.github.io/grok-bedrock-xapi-benchmark/blog/}
}</code></pre></div><h2 id="references">
                  Further reading
                  <a aria-label="Link to section" class="heading-anchor" href="#references">#</a>
                </h2><ol class="references-list"><li id="ref-aws-grok-bedrock-announcement">AWS (2026). <em>Grok 4.3 from xAI now available in Amazon Bedrock.</em> Official AWS announcement covering regional availability and Bedrock Mantle integration. <a href="https://aws.amazon.com/about-aws/whats-new/2026/06/grok-amazon-bedrock/">aws.amazon.com</a></li><li id="ref-xai-grok-bedrock">xAI (2026). <em>Grok on Amazon Bedrock.</em> xAI's launch post, including model positioning and benchmark claims. <a href="https://x.ai/news/grok-amazon-bedrock">x.ai</a></li><li id="ref-grok43-model-card">AWS (2026). <em>Grok 4.3 model card.</em> Amazon Bedrock User Guide covering endpoints, reasoning-effort configuration, service tiers, and quotas for the Mantle path. <a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-xai-grok-4-3.html">docs.aws.amazon.com</a></li><li id="ref-xai-api-docs">xAI (2026). <em>API documentation.</em> Reference for the first-party path used in this comparison. <a href="https://docs.x.ai/">docs.x.ai</a></li><li id="ref-gsm8k-paper">Cobbe et al. (2021). <em>Training Verifiers to Solve Math Word Problems.</em> The paper that introduced GSM8K. <a href="https://arxiv.org/abs/2110.14168">arxiv.org</a></li><li id="ref-gsm8k-dataset">OpenAI (2021). <em>GSM8K dataset.</em> The openly available test split used in this benchmark. <a href="https://github.com/openai/grade-school-math">github.com</a></li></ol></article>

      <nav class="paginator" aria-label="Post navigation">
        <a href="#top">back to top</a>
      </nav>
    </main>

    <footer class="site-footer">
      Nina Cvetkovska &middot; Bojan Jakimovski &middot;
      <svg class="loka-logo" viewBox="0 0 102 33" role="img" aria-label="Loka"><use href="#loka-wordmark" /></svg> &middot; 2026
    </footer>
  </div>

  <button class="theme-toggle" id="theme-toggle" type="button" aria-label="Toggle dark mode" aria-pressed="false" title="Toggle dark mode">
    <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" /></svg>
    <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4" /><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" /></svg>
  </button>

  <button class="back-to-top" id="back-to-top" aria-label="Back to top">&#8593;</button>


  <script>

    (function () {
      var progress = document.getElementById("reading-progress");
      function updateProgress() {
        var h = document.documentElement.scrollHeight - window.innerHeight;
        progress.style.width = (h > 0 ? (window.scrollY / h) * 100 : 0) + "%";
      }
      var btn = document.getElementById("back-to-top");
      function updateBtn() { btn.classList.toggle("visible", window.scrollY > 400); }
      btn.addEventListener("click", function () { window.scrollTo({ top: 0, behavior: "smooth" }); });


      var themeBtn = document.getElementById("theme-toggle");
      function syncTheme() {
        themeBtn.setAttribute("aria-pressed", document.documentElement.getAttribute("data-theme") === "dark" ? "true" : "false");
      }
      themeBtn.addEventListener("click", function () {
        var dark = document.documentElement.getAttribute("data-theme") === "dark";
        if (dark) { document.documentElement.removeAttribute("data-theme"); }
        else { document.documentElement.setAttribute("data-theme", "dark"); }
        try { localStorage.setItem("loka-theme", dark ? "light" : "dark"); } catch (e) {}
        syncTheme();
      });
      syncTheme();

      var tocLinks = document.querySelectorAll(".toc-list a");
      var sections = [];
      tocLinks.forEach(function (link) {
        var el = document.getElementById(link.getAttribute("href").slice(1));
        if (el) sections.push({ el: el, link: link });
      });
      function updateToc() {
        var pos = window.scrollY + 100, current = null;
        for (var i = sections.length - 1; i >= 0; i--) {
          if (sections[i].el.offsetTop <= pos) { current = sections[i]; break; }
        }
        tocLinks.forEach(function (l) { l.classList.remove("active"); });
        if (current) current.link.classList.add("active");
      }
      var ticking = false;
      window.addEventListener("scroll", function () {
        if (!ticking) {
          requestAnimationFrame(function () { updateProgress(); updateBtn(); updateToc(); ticking = false; });
          ticking = true;
        }
      }, { passive: true });
      updateProgress(); updateBtn(); updateToc();
    })();


    (function () {
      if (typeof Chart === "undefined") return;


      var C = {
        blue: "#1877f2",
        teal: "#0c6f71",
        yellow: "#f7d043",
        black: "#050517",
        red: "#ef4444",
        purple: "#8b5cf6",
        gray: "#6b7280",
        awsPath: "#1877F2",
        awsPathSoft: "rgba(24, 119, 242, 0.45)",
        xaiPath: "#0C6F71",
        xaiPathSoft: "rgba(12, 111, 113, 0.45)",
      };
      var FONT = { family: "'Alliance No.2', 'Archivo', -apple-system, 'Segoe UI', 'Helvetica Neue', Helvetica, sans-serif" };


      function lineDataset(points, label, color, opts) {
        opts = opts || {};
        return {
          label: label, data: points, borderColor: color,
          backgroundColor: "transparent", borderWidth: opts.thin ? 1 : 2,
          pointRadius: 0, pointHoverRadius: 4, tension: 0.3,
          borderDash: opts.dash || [],
        };
      }

      function lineChartOpts(yLabel, xLabel) {
        return {
          responsive: true, maintainAspectRatio: true,
          interaction: { mode: "index", intersect: false },
          plugins: {
            legend: { position: "top", labels: { font: { family: FONT.family, size: 12 }, usePointStyle: true, pointStyle: "line", boxWidth: 24 } },
            title: { display: false },
            tooltip: { titleFont: { family: FONT.family }, bodyFont: { family: FONT.family } },
          },
          scales: {
            x: { type: "linear", title: { display: true, text: xLabel || "x", font: { family: FONT.family, size: 12 } }, grid: { display: false }, ticks: { font: { family: FONT.family, size: 11 } } },
            y: { title: { display: true, text: yLabel || "y", font: { family: FONT.family, size: 12 } }, grid: { display: false }, ticks: { font: { family: FONT.family, size: 11 } } },
          },
        };
      }

      function barChartOpts(xLabel, yLabel) {
        return {
          responsive: true, maintainAspectRatio: true,
          plugins: { legend: { display: false }, tooltip: { titleFont: { family: FONT.family }, bodyFont: { family: FONT.family } } },
          scales: {
            x: { title: { display: true, text: xLabel, font: { family: FONT.family, size: 12 } }, grid: { display: false }, ticks: { font: { family: FONT.family, size: 11 } } },
            y: { title: { display: true, text: yLabel, font: { family: FONT.family, size: 12 } }, grid: { display: false }, ticks: { font: { family: FONT.family, size: 11 } } },
          },
        };
      }

        var efforts = ["none", "low", "medium", "high"];

        var accuracyEl = document.getElementById("chart-accuracy");
        if (accuracyEl) {
          new Chart(accuracyEl, {
            type: "line",
            data: {
              labels: efforts,
              datasets: [
                { label: "Bedrock", data: [93.10, 96.51, 96.59, 96.13], borderColor: C.awsPath, backgroundColor: "transparent", borderWidth: 2, pointRadius: 3, tension: 0.25 },
                { label: "xAI API", data: [94.16, 96.89, 96.97, 96.89], borderColor: C.xaiPath, backgroundColor: "transparent", borderWidth: 2, pointRadius: 3, tension: 0.25 },
              ],
            },
            options: {
              responsive: true,
              maintainAspectRatio: true,
              interaction: { mode: "index", intersect: false },
              plugins: {
                legend: { position: "top", labels: { font: { family: FONT.family, size: 12 }, usePointStyle: true, pointStyle: "line", boxWidth: 24 } },
                tooltip: { callbacks: { label: function (ctx) { return ctx.dataset.label + ": " + ctx.parsed.y.toFixed(2) + "%"; } } },
              },
              scales: {
                x: { title: { display: true, text: "Reasoning effort", font: { family: FONT.family, size: 12 } }, grid: { display: false }, ticks: { font: { family: FONT.family, size: 11 } } },
                y: { min: 92, max: 98, title: { display: true, text: "Accuracy (%)", font: { family: FONT.family, size: 12 } }, grid: { display: false }, ticks: { font: { family: FONT.family, size: 11 } } },
              },
            },
          });
        }

        var gainLatencyEl = document.getElementById("chart-gain-latency");
        if (gainLatencyEl) {
          var gainLatencyPoints = [
            { x: 2.77, y: 3.41, provider: "Bedrock", step: "none → low" },
            { x: 0.25, y: 0.08, provider: "Bedrock", step: "low → medium" },
            { x: -0.20, y: -0.46, provider: "Bedrock", step: "medium → high" },
            { x: 1.76, y: 2.73, provider: "xAI API", step: "none → low" },
            { x: 1.06, y: 0.08, provider: "xAI API", step: "low → medium" },
            { x: -0.09, y: -0.08, provider: "xAI API", step: "medium → high" },
          ];
          new Chart(gainLatencyEl, {
            type: "scatter",
            data: {
              datasets: [
                {
                  label: "Bedrock",
                  data: gainLatencyPoints.filter(function (p) { return p.provider === "Bedrock"; }),
                  borderColor: C.awsPath,
                  backgroundColor: C.awsPath,
                  pointRadius: 5,
                  pointHoverRadius: 7,
                },
                {
                  label: "xAI API",
                  data: gainLatencyPoints.filter(function (p) { return p.provider === "xAI API"; }),
                  borderColor: C.xaiPath,
                  backgroundColor: C.xaiPath,
                  pointRadius: 5,
                  pointHoverRadius: 7,
                },
              ],
            },
            options: {
              responsive: true,
              maintainAspectRatio: true,
              interaction: { mode: "nearest", intersect: true },
              plugins: {
                legend: { position: "top", labels: { font: { family: FONT.family, size: 12 }, usePointStyle: true } },
                tooltip: {
                  callbacks: {
                    title: function (items) {
                      var p = items[0].raw;
                      return p.provider + " · " + p.step;
                    },
                    label: function (ctx) {
                      return "Accuracy gain: " + ctx.raw.y.toFixed(2) + " pp, added p50 latency: " + ctx.raw.x.toFixed(2) + "s";
                    },
                  },
                },
              },
              scales: {
                x: {
                  title: { display: true, text: "Added median latency (seconds)", font: { family: FONT.family, size: 12 } },
                  grid: { display: false },
                  ticks: { font: { family: FONT.family, size: 11 } },
                },
                y: {
                  title: { display: true, text: "Accuracy gain (percentage points)", font: { family: FONT.family, size: 12 } },
                  grid: { display: false },
                  ticks: { font: { family: FONT.family, size: 11 } },
                },
              },
            },
          });
        }

        var latencyEl = document.getElementById("chart-latency");
        if (latencyEl) {
          new Chart(latencyEl, {
            type: "bar",
            data: {
              labels: efforts,
              datasets: [
                { label: "Bedrock p50", data: [0.79, 3.56, 3.81, 3.61], backgroundColor: C.awsPath, borderRadius: 3 },
                { label: "Bedrock p95", data: [1.72, 9.70, 11.24, 10.41], backgroundColor: C.awsPathSoft, borderRadius: 3 },
                { label: "xAI p50", data: [1.05, 2.81, 3.87, 3.78], backgroundColor: C.xaiPath, borderRadius: 3 },
                { label: "xAI p95", data: [1.56, 5.45, 9.66, 9.96], backgroundColor: C.xaiPathSoft, borderRadius: 3 },
              ],
            },
            options: {
              responsive: true,
              maintainAspectRatio: true,
              plugins: {
                legend: { position: "top", labels: { font: { family: FONT.family, size: 12 } } },
                tooltip: { callbacks: { label: function (ctx) { return ctx.dataset.label + ": " + ctx.parsed.y.toFixed(2) + "s"; } } },
              },
              scales: {
                x: { title: { display: true, text: "Reasoning effort", font: { family: FONT.family, size: 12 } }, grid: { display: false }, ticks: { font: { family: FONT.family, size: 11 } } },
                y: { title: { display: true, text: "Seconds", font: { family: FONT.family, size: 12 } }, grid: { display: false }, ticks: { font: { family: FONT.family, size: 11 } } },
              },
            },
          });
        }

        var reasoningEl = document.getElementById("chart-reasoning");
        if (reasoningEl) {
          new Chart(reasoningEl, {
            type: "line",
            data: {
              labels: efforts,
              datasets: [
                { label: "Bedrock", data: [0, 289, 467, 459], borderColor: C.awsPath, backgroundColor: "transparent", borderWidth: 2, pointRadius: 3, tension: 0.25 },
                { label: "xAI API", data: [0, 264, 399, 412], borderColor: C.xaiPath, backgroundColor: "transparent", borderWidth: 2, pointRadius: 3, tension: 0.25 },
              ],
            },
            options: {
              responsive: true,
              maintainAspectRatio: true,
              interaction: { mode: "index", intersect: false },
              plugins: { legend: { position: "top", labels: { font: { family: FONT.family, size: 12 }, usePointStyle: true, pointStyle: "line", boxWidth: 24 } } },
              scales: {
                x: { title: { display: true, text: "Reasoning effort", font: { family: FONT.family, size: 12 } }, grid: { display: false }, ticks: { font: { family: FONT.family, size: 11 } } },
                y: { title: { display: true, text: "Median reasoning tokens", font: { family: FONT.family, size: 12 } }, grid: { display: false }, ticks: { font: { family: FONT.family, size: 11 } } },
              },
            },
          });
        }
    })();
  </script>
</body>

</html>]]></content><author><name>shekswess</name></author><category term="AI" /><category term="LLM" /><category term="xAI" /><category term="Grok 4.3" /><category term="Amazon Bedrock" /><category term="Bedrock Mantle" /><category term="Benchmarking" /><category term="AWS" /><summary type="html"><![CDATA[A benchmark of Grok 4.3 through Amazon Bedrock Mantle and the xAI API, covering accuracy, latency, cost, reliability, and enterprise deployment tradeoffs.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://shekswess.github.io/assets/images/grok-benchmark/background.png" /><media:content medium="image" url="https://shekswess.github.io/assets/images/grok-benchmark/background.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How Loka Built a Natural, Low-Latency Voice Agent with Amazon Nova 2 Sonic</title><link href="https://shekswess.github.io/how-loka-built-a-natural-low-latency-voice-agent-with-amazon-nova-2-sonic.html" rel="alternate" type="text/html" title="How Loka Built a Natural, Low-Latency Voice Agent with Amazon Nova 2 Sonic" /><published>2026-06-23T16:00:00+00:00</published><updated>2026-06-23T16:00:00+00:00</updated><id>https://shekswess.github.io/how-loka-built-a-natural-low-latency-voice-agent-with-amazon-nova-2-sonic</id><content type="html" xml:base="https://shekswess.github.io/how-loka-built-a-natural-low-latency-voice-agent-with-amazon-nova-2-sonic.html"><![CDATA[<p><a href="https://www.loka.com/">Loka</a> transformed customer voice interactions by building a conversational AI agent with <a href="https://nova.amazon.com/sonic">Amazon Nova 2 Sonic</a> that keeps customers engaged with natural, responsive experiences. Their AWS-based solution achieves high speech reasoning accuracy on <a href="https://huggingface.co/datasets/ArtificialAnalysis/big_bench_audio">Big Bench Audio</a> while delivering significantly lower costs and faster response times than traditional voice AI pipelines. In this post, we demonstrate the architecture and approach Loka used to solve a common frustration: robotic, slow voice assistants that cause customers to hang up, damaging brand reputation and driving up support costs.</p>

<h2 id="why-traditional-voice-assistants-fall-short">Why traditional voice assistants fall short</h2>

<p>Traditional voice assistants follow a three-step process that creates the fundamental problem. First, they convert your speech into text using Speech-to-Text systems. Next, they process that text through a Large Language Model (LLM). Finally, they convert the text response back into speech using Text-to-Speech technology. This pipeline introduces compounding delays at every step. The result is often a 3 to 5 second pause before you hear a response. That delay destroys the feeling of natural conversation. It makes interrupting or correcting the assistant feel clunky and frustrating.</p>

<p>Consider a real scenario at an automotive dealership. A customer calls and says, <em>“I’m looking for that SUV you advertised, but not the hybrid one. I can only come in after 5 PM.”</em> The assistant needs to parse multiple pieces of information simultaneously. It must understand the intent, negation, and scheduling constraints. Traditional systems struggle with this complexity because they lose crucial information during conversion. Tone, hesitation, and urgency disappear when speech becomes text. The dealership context makes these limitations painfully clear. Customers expect immediate, helpful responses when they call. A five-second pause feels like an eternity in a sales conversation. Worse, if the assistant misunderstands and needs clarification, delays compound. The conversation becomes tedious rather than helpful.</p>

<p>Beyond the technical delays, there’s an economic problem. Serving thousands of locations requires strict cost control. Traditional real-time voice systems can become cost-prohibitive at scale, particularly when processing continuous audio streams. The combination of poor experience and high cost has limited voice AI adoption. Businesses need a better solution.</p>

<h2 id="native-speech-to-speech-models">Native speech-to-speech models</h2>

<p>Recent advances in AI have unlocked a fundamentally different approach. Developers can now send audio streams directly to speech-to-speech models that handle understanding, reasoning, and generation as a unified system. By processing audio end-to-end, these models capture tone, emotion, and subtle cues that traditional text-only pipelines miss.</p>

<p>To validate this approach, rigorous testing was essential. We used <a href="https://huggingface.co/datasets/ArtificialAnalysis/big_bench_audio">Big Bench Audio</a>, a benchmark that measures reasoning over speech inputs. <a href="https://nova.amazon.com/sonic">Amazon Nova 2 Sonic</a> achieved a speech reasoning score of 87.0. This outperformed <a href="https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/2-5-flash-live-api">Gemini 2.5 Flash Native Audio (Live API)</a> at 71.0 and exceeded <a href="https://developers.openai.com/api/docs/models/gpt-realtime">GPT Realtime</a>’s 83.0. These scores confirmed that native audio processing doesn’t sacrifice intelligence for speed. The model could handle complex, multi-part requests in real dealership scenarios.</p>

<p><img src="https://d2908q01vomqb2.cloudfront.net/f1f836cb4ea6efb2a0b1b99f41ad8b103eff4b59/2026/06/02/ML-20376-1.png" alt="Speech reasoning scores comparison across models on Big Bench Audio benchmark" /></p>

<p><em>Figure 1 – Speech reasoning scores comparison – Big Bench Audio</em></p>

<p>Reasoning ability alone isn’t enough for production systems. Latency determines whether conversations feel natural or robotic. Nova 2 Sonic achieved Time to First Audio of 1.39 seconds. This response time allows for natural “barge-in” behavior. When users interrupt the conversation, the voice agent responded naturally. This experience matches human conversation patterns.</p>

<p><img src="https://d2908q01vomqb2.cloudfront.net/f1f836cb4ea6efb2a0b1b99f41ad8b103eff4b59/2026/06/02/ML-20376-2.png" alt="Time to first audio comparison across models on Big Bench Audio benchmark" /></p>

<p><em>Figure 2 – Time to first Audio comparison – Big Bench Audio</em></p>

<p>Cost efficiency also improved. <a href="https://aws.amazon.com/nova/pricing/">Nova 2 Sonic costs approximately $0.27 per hour</a> of the input audio (based on pricing as of the time of publishing). This is lower than comparable real-time models and traditional methods.</p>

<p><img src="https://d2908q01vomqb2.cloudfront.net/f1f836cb4ea6efb2a0b1b99f41ad8b103eff4b59/2026/06/02/ML-20376-3.png" alt="Cost per hour of audio comparison across models on Big Bench Audio benchmark" /></p>

<p><em>Figure 3 – Cost per hour of audio comparison – Big Bench Audio</em></p>

<p>To measure quality beyond latency and cost, we needed a structured evaluation. We built an automated pipeline using LLM as a judge. Each conversation was scored on five dimensions using a 1-5 scale. Response Appropriateness measured whether replies were relevant and contextually correct. Intent Understanding assessed whether the agent grasped the user’s underlying goal. Completeness tracked required information or actions were provided. Conversational Naturalness measured flow, turn-taking, tone, and human-likeness.</p>

<p>Comparing Amazon Nova Sonic to Amazon Nova 2 Sonic revealed clear progress. Response Appropriateness improved from 2.5 to 2.9. Intent Understanding rose from 2.9 to 3.0. Most significantly, Completeness jumped from 1.8 to 2.5. This meant agents were far more likely to finish complex tasks. Conversational Naturalness improved from 2.5 to 2.8. The overall score increased from 2.4 to 2.7. These gains translated directly to better customer outcomes at dealerships.</p>

<table>
  <thead>
    <tr>
      <th><strong>Metric (1-5 Scale)</strong></th>
      <th><strong>Nova Sonic (Baseline)</strong></th>
      <th><strong>Nova 2 Sonic</strong></th>
      <th><strong>Change</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Response Appropriateness</strong></td>
      <td>2.5</td>
      <td><strong>2.9</strong></td>
      <td><strong>+0.4</strong></td>
    </tr>
    <tr>
      <td><strong>Intent Understanding</strong></td>
      <td>2.9</td>
      <td><strong>3.0</strong></td>
      <td><strong>+0.1</strong></td>
    </tr>
    <tr>
      <td><strong>Completeness</strong></td>
      <td>1.8</td>
      <td><strong>2.5</strong></td>
      <td><strong>+0.7</strong></td>
    </tr>
    <tr>
      <td><strong>Conversational Naturalness</strong></td>
      <td>2.5</td>
      <td><strong>2.8</strong></td>
      <td><strong>+0.3</strong></td>
    </tr>
    <tr>
      <td><strong>Overall Judge Score</strong></td>
      <td>2.4</td>
      <td><strong>2.7</strong></td>
      <td><strong>+0.3</strong></td>
    </tr>
  </tbody>
</table>

<p><em>Table 1 – Speech-to-speech model metrics comparison on the five dimensions</em></p>

<h2 id="engineering-conversational-ai-agents">Engineering conversational AI agents</h2>

<p>With a strong foundation model, the next challenge was optimization. We treated prompts like code, iterating based on measured performance. The baseline Nova 2 Sonic configuration scored 2.7 overall. After the first prompt refinement, the score rose to 3.1. The second iteration achieved 3.8 out of 5.0. This improvement came from better turn discipline and repetition control. The agent learned when to speak, when to listen, and when to ask clarifying questions.</p>

<table>
  <thead>
    <tr>
      <th><strong>Configuration</strong></th>
      <th><strong>Response</strong><br /><strong>Appropriateness</strong></th>
      <th><strong>Intent Understanding</strong></th>
      <th><strong>Completeness</strong></th>
      <th><strong>Error Recovery</strong></th>
      <th><strong>Conversational</strong><br /><strong>Naturalness</strong></th>
      <th><strong>Overall</strong><br /><strong>Judge Score</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Amazon Nova 2 Sonic (Baseline)</strong></td>
      <td>2.9</td>
      <td>3.0</td>
      <td>2.5</td>
      <td>2.6</td>
      <td>2.8</td>
      <td>2.7</td>
    </tr>
    <tr>
      <td><strong>Amazon Nova 2 Sonic (Prompt v1)</strong></td>
      <td>3.2</td>
      <td>3.3</td>
      <td>3.0</td>
      <td>2.8</td>
      <td>3.9</td>
      <td>3.1</td>
    </tr>
    <tr>
      <td><strong>Amazon Nova 2 Sonic (Prompt v2)</strong></td>
      <td><strong>3.7</strong></td>
      <td><strong>3.9</strong></td>
      <td><strong>3.9</strong></td>
      <td><strong>3.8</strong></td>
      <td><strong>4.1</strong></td>
      <td><strong>3.8</strong></td>
    </tr>
  </tbody>
</table>

<p><em>Table 2 – Speech-to-speech model metrics comparison on the five dimensions after prompt enhancements</em></p>

<p>The team enhanced the baseline prompt in several ways, evolving it into two prompt templates. They replaced hardcoded dealership details with templatized variables like <code class="language-plaintext highlighter-rouge">{assistant_name}</code> and <code class="language-plaintext highlighter-rouge">{dealership_address}</code>. This change made the prompt reusable across any dealership.</p>

<p>The team shifted formatting from numbered lists to bullet points under clearly labeled headings. Headings like “Tool Usage Rules,” “Error Recovery,” and “Conversation Endings” gave the model sharper behavioral boundaries. This structure reduced instruction bleed between topics.</p>

<p>The team added concrete behavior examples throughout the prompt. These examples showed exactly how to acknowledge without echoing the caller. They also introduced a pre-response checklist that prompts the model to self-audit before every reply.</p>

<p><a href="https://aws.amazon.com/bedrock/prompt-management/">Amazon Bedrock Prompt Management</a> became a natural home for this entire lifecycle. It allowed the team to store each template version with a unique Amazon Resource Name (ARN). They could promote changes from draft to production without touching application code.</p>

<p>When a new dealership was onboarded, their specific variables were injected at runtime through the <a href="https://aws.amazon.com/bedrock/">Amazon Bedrock</a> API. This meant the same core prompt served every client while remaining fully customizable.</p>

<p>The team added <a href="https://aws.amazon.com/iam/">AWS Identity and Access Management (AWS IAM)</a> access controls to restrict who could author, approve, or deploy prompt changes. This brought a proper governance layer to what was previously an informal editing process.</p>

<p>This approach turned prompt engineering from a one-off task into a repeatable, auditable workflow. The workflow scaled as the number of dealerships and use cases grew.</p>

<p>Real-world testing required edge cases that mirror actual dealership calls. We tested angry customers, busy parents, chatty customers, confused customers, and elderly callers.</p>

<p>The Busy Parent scenario scored 5.0 across the five dimensions. The agent handled interruptions, background noise, and time pressure flawlessly. The Angry Customer case scored 4.5 overall. The agent remained calm, empathetic, and solution focused. The Confused Customer scenario also scored 4.5. The agent patiently clarified without sounding condescending.</p>

<table>
  <thead>
    <tr>
      <th><strong>Chat Examples</strong></th>
      <th><strong>Response Appropriateness</strong></th>
      <th><strong>Intent Understanding</strong></th>
      <th><strong>Completeness</strong></th>
      <th><strong>Error Recovery</strong></th>
      <th><strong>Conversational Naturalness</strong></th>
      <th><strong>Overall **</strong>Judge Score**</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Angry Customer</strong></td>
      <td>4.5</td>
      <td>4.5</td>
      <td>4.0</td>
      <td>4.5</td>
      <td>4.5</td>
      <td>4.5</td>
    </tr>
    <tr>
      <td><strong>Busy Parent Customer</strong></td>
      <td>5.0</td>
      <td>5.0</td>
      <td>5.0</td>
      <td>5.0</td>
      <td>5.0</td>
      <td>5.0</td>
    </tr>
    <tr>
      <td><strong>Chatty Customer</strong></td>
      <td>3.5</td>
      <td>3.0</td>
      <td>2.5</td>
      <td>2.0</td>
      <td>4.0</td>
      <td>3.0</td>
    </tr>
    <tr>
      <td><strong>Confused Customer</strong></td>
      <td>4.5</td>
      <td>4.5</td>
      <td>4.5</td>
      <td>5.0</td>
      <td>4.5</td>
      <td>4.5</td>
    </tr>
    <tr>
      <td><strong>Elderly Customer</strong></td>
      <td>3.5</td>
      <td>3.0</td>
      <td>2.5</td>
      <td>2.0</td>
      <td>4.0</td>
      <td>3.0</td>
    </tr>
    <tr>
      <td><strong>Average</strong></td>
      <td><strong>4.2</strong></td>
      <td><strong>4.0</strong></td>
      <td><strong>3.7</strong></td>
      <td><strong>3.7</strong></td>
      <td><strong>4.4</strong></td>
      <td><strong>4.0</strong></td>
    </tr>
  </tbody>
</table>

<p><em>Table 3 – Speech to Speech model evaluation based on customer personas</em></p>

<p>Two scenarios revealed the remaining opportunities. The Chatty Customer and Elderly Customer cases both scored 3.0 overall. When users provided long, meandering inputs, the voice agent struggled with the structure. Completeness scores dropped to 2.5 in these situations. Error recovery fell to 2.0. These results identified clear areas for future prompt engineering. Still, the average edge-case score of 4.0 indicated strong real-world readiness.</p>

<p>Building the right architecture was important for production deployment. We designed a serverless, event-driven system using <a href="https://livekit.io/">LiveKit</a> as the transport layer. LiveKit abstracts the complexities of <a href="https://webrtc.org/">WebRTC</a> for web clients and <a href="https://www.sipforum.org/technology/sip-technology/">Session Initiation Protocol (SIP)</a> for phone calls. This allowed the engineering team to focus entirely on agent logic.</p>

<p><img src="https://d2908q01vomqb2.cloudfront.net/f1f836cb4ea6efb2a0b1b99f41ad8b103eff4b59/2026/06/02/ML-20376-4.png" alt="Conversational AI assistant solution architecture showing LiveKit, AWS Fargate, Amazon Bedrock, and supporting services" /></p>

<p><em>Figure 4 – Conversational AI assistant – Solution architecture</em></p>

<p>The voice agent integrates with dealership operations through Python function-based tools, which represent actions the assistant can perform during a conversation. Common tools include inventory search, appointment booking, and customer data lookup. These tools act as the integration layer between Amazon Nova 2 Sonic and backend services. The model decides when a tool should be used, and the Python function executes the corresponding GraphQL query or mutation, returning structured data back to the agent.</p>

<p><a href="https://aws.amazon.com/fargate/">AWS Fargate</a> provided the compute layer. We containerized LiveKit Agents on <a href="https://aws.amazon.com/pm/ecs">Amazon Elastic Container Service (Amazon ECS)</a>. This enabled independent scaling of agent workers versus media servers. Resources could be optimized dynamically during peak dealership hours. <a href="https://aws.amazon.com/rds/">Amazon Relational Database Service (Amazon RDS)</a> was used as the persistent relational store for structured application data including dealership configurations, conversation history, and customer records.</p>

<p>Real-time voice agents cannot tolerate database latency. <a href="https://aws.amazon.com/elasticache/">Amazon ElastiCache</a> is used to handle room management and ephemeral session coordination across distributed tasks. Amazon Bedrock provided direct access to the Nova 2 Sonic model.</p>

<p>Browser clients connected using <a href="https://webrtc.org/">WebRTC</a> (Web Real-Time Communications), open-source framework for peer-to-peer audio, video and data transmission. Traditional phone calls entered through a SIP Trunk routed via Network Load Balancer. This allowed TCP/UDP throughput for media packets.</p>

<p>Observability came from <a href="https://langfuse.com/">Langfuse</a>, self-hosted on AWS. It traced every agent decision and tool call. This data was fed back into the evaluation pipeline for continuous improvement.</p>

<h2 id="demo">Demo</h2>

<p>The following video demonstrates the functionality.</p>

<figure class="post-video">
<video controls="" preload="metadata">
<source src="https://d2908q01vomqb2.cloudfront.net/artifacts/DBSBlogs/ML-20376/demo.mp4?_=1" type="video/mp4" />
</video>
</figure>

<h2 id="a-new-standard-for-conversational-ai">A new standard for conversational AI</h2>

<p>The transition from text-based chatbots to real-time voice agents represents more than an interface change. It requires fundamentally different infrastructure and thinking. Nova 2 Sonic hits three critical engineering requirements simultaneously. First, it provides high reasoning capability without intermediate text conversion. Second, it delivers low latency that enables natural, human-like interruption. Third, it offers production viability with cost-effectiveness for thousands of locations.</p>

<p>For automotive dealerships, this technology is already driving revenue in production. Customers receive immediate, helpful responses when they call. Complex requests get handled smoothly in a single conversation. Appointments get scheduled correctly without frustrating back-and-forth exchanges.</p>

<p>The implications extend far beyond automotive sales. Industries requiring real-time, intelligent voice interactions can benefit. Imagine a travel agent that helps you plan an entire holiday through natural conversation. Picture an educational tutor that adapts to your speaking style and learning pace. Consider healthcare scheduling systems that handle complex insurance and availability questions.</p>

<p>Speech-to-speech AI has reached production readiness. Start experimenting with Nova 2 Sonic in your own AWS environment. Build prototypes, test edge cases, and explore what’s possible for your business.</p>

<p>This post represents a collaboration between AWS and Loka, who specialize in building production-ready AI solutions. Loka’s team has already solved the hard problems around architecture, evaluation, and optimization. Their automotive dealership experience translates directly to other industries.</p>

<p>Speech-to-speech AI is still in its early days. The best applications haven’t been invented yet. Your industry knowledge combined with this technology could create breakthrough solutions. Start your journey with <a href="https://nova.amazon.com/sonic">Nova 2 Sonic</a> and <a href="https://partners.amazonaws.com/partners/0010h00001hfV65AAE/Loka">Loka</a> today. Transform how your customers experience conversation with your business.</p>

<h2 id="about-the-authors">About the authors</h2>

<p><strong>Bojan Jakimovski</strong></p>

<p>Bojan Jakimovski is a Machine Learning Lead at Loka, an AWS Ambassador, and a 9x AWS Certified professional. He holds a Master’s degree in Electrical Engineering and Information Technologies from FEEIT, specializing in Dedicated Computer Systems, and brings expertise across machine learning, deep learning, MLOps, and cloud-native architectures on AWS. As a hands-on practitioner, he designs and deploys scalable AI systems, with a strong focus on Generative AI, distributed training, and production-grade ML infrastructure. His interests also extend to high-performance computing, federated learning and real-time sensitive systems, where he continues to explore efficient and scalable approaches to modern AI systems.</p>

<p><strong>Nina Cvetkovska</strong></p>

<p>Nina Cvetkovska is a Machine Learning Engineer at Loka and an AWS Certified Machine Learning Specialist. She holds a background in Software Engineering from FCSE, bringing a strong academic foundation to her hands-on work in ML and AI. As a practitioner, Nina focuses on building and deploying machine learning solutions with interests in computer vision, edge AI, and real-time multimodal systems, including speech-to-speech applications, where she explores efficient and low-latency approaches to modern AI.</p>

<p><strong>Venkat Gomatham</strong></p>

<p>Venkat Gomatham is a Senior Partner Solutions Architect at AWS, providing strategic guidance to partners in their cloud transformation journey. With 22+ years as an IT architect, he drives innovation and digital transformation initiatives, helping organizations modernize their IT landscapes through cutting-edge technologies like Agentic and Physical AI.</p>

<p><strong>Arabinda Pani</strong></p>

<p>Arabinda Pani is a Principal Generative AI Specialist Solutions Architect at AWS, where he helps enterprise customers and strategic partners design, build, and scale generative AI and agentic AI solutions. As part of the AMER Generative AI Specialist PSA team, he leads technical enablement and thought leadership initiatives centered on Amazon Bedrock, Amazon Nova, and agentic AI — driving significant partner adoption and business impact across the Americas. With 22+ years of IT experience spanning database engineering, cloud architecture, and AI/ML, Arabinda brings deep technical expertise and a strong track record of translating complex AI capabilities into real-world business value. He holds an MBA from San Diego State University, a B.Tech. from NIT Warangal, and is an AWS Certified Solutions Architect – Professional.</p>]]></content><author><name>shekswess</name></author><category term="AI" /><category term="LLM" /><category term="AWS" /><category term="Amazon Nova" /><category term="Nova 2 Sonic" /><category term="Voice AI" /><category term="Conversational AI" /><category term="Bedrock" /><category term="LiveKit" /><summary type="html"><![CDATA[In this post, we demonstrate the architecture and approach Loka used to solve a common frustration: robotic, slow voice assistants that cause customers to hang up, damaging brand reputation and driving up support costs.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://d2908q01vomqb2.cloudfront.net/f1f836cb4ea6efb2a0b1b99f41ad8b103eff4b59/2026/06/02/20376.png" /><media:content medium="image" url="https://d2908q01vomqb2.cloudfront.net/f1f836cb4ea6efb2a0b1b99f41ad8b103eff4b59/2026/06/02/20376.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Benchmarking GPT-5.5 on Amazon Bedrock vs. the OpenAI API: The Engineering Walkthrough</title><link href="https://shekswess.github.io/benchmarking-gpt-5-5-on-amazon-bedrock-vs-the-openai-api-the-engineering-walkthrough.html" rel="alternate" type="text/html" title="Benchmarking GPT-5.5 on Amazon Bedrock vs. the OpenAI API: The Engineering Walkthrough" /><published>2026-06-14T16:00:00+00:00</published><updated>2026-06-14T16:00:00+00:00</updated><id>https://shekswess.github.io/benchmarking-gpt-5-5-on-amazon-bedrock-vs-the-openai-api-the-engineering-walkthrough</id><content type="html" xml:base="https://shekswess.github.io/benchmarking-gpt-5-5-on-amazon-bedrock-vs-the-openai-api-the-engineering-walkthrough.html"><![CDATA[<p><em><strong>Original Source of the blog post: <a href="https://medium.com/loka-engineering/benchmarking-gpt-5-5-on-amazon-bedrock-vs-the-openai-api-the-engineering-walkthrough-3d57899fc058">Benchmarking GPT-5.5 on Amazon Bedrock vs. the OpenAI API: The Engineering Walkthrough</a></strong></em></p>

<p><em><strong>Written by Petar Kalinovski and Bojan Jakimovski</strong></em></p>

<h2 id="the-engineering-walkthrough">The Engineering Walkthrough</h2>

<p>The main post covers what we found: GPT-5.5 on Amazon Bedrock matches OpenAI API accuracy and comes out faster on every latency and throughput dimension we measured. This post covers how we ran it. If you want to reproduce the GSM8K numbers or adapt the harness for your own workload, this is the companion piece.</p>

<p>The concrete question behind the benchmark was simple:</p>

<blockquote>
  <p>Does routing GPT-5.5 through Amazon Bedrock Mantle cost you anything in answer quality, latency, or reliability compared to the native OpenAI API?</p>
</blockquote>

<p>We ran the full GSM8K test split, 1,319 grade-school math problems, through both provider paths at three reasoning levels (<code class="language-plaintext highlighter-rouge">medium</code>, <code class="language-plaintext highlighter-rouge">high</code>, <code class="language-plaintext highlighter-rouge">xhigh</code>), recording per-request streaming latency, output throughput, token usage, and failures. The benchmark harness is open and the results are saved in the repo. This post walks through the setup, the exact commands, the output format, and the design choices that made the comparison clean.</p>

<p>For the main findings, including the accuracy tables, latency charts, and the two regulated-industry copilots we built on the path that passed, read the main Loka writeup here: <a href="https://lokahq.github.io/gpt-bedrock-openai-benchmark/blog/">How Loka Evaluates and Builds with Frontier Models on AWS</a></p>

<h2 id="why-gsm8k">Why GSM8K</h2>

<p>We needed a dataset that was long enough to produce stable latency distributions (1,319 requests per run means the p90 and p95 figures mean something), short enough to complete in a single session without batching or parallelism, and one where answer correctness is unambiguous and automatable. GSM8K satisfies all three. The final <code class="language-plaintext highlighter-rouge">####</code> answer format makes accuracy scoring a string comparison after normalization, with no LLM judge in the loop.</p>

<p>The math content also makes token budgets predictable. Each question fits in well under a hundred tokens. Reasoning answers are short — a few lines of working plus a numeric result. That keeps the benchmark from inflating latency through unusual output lengths and keeps costs bounded before you start.</p>

<h2 id="tested-setup">Tested Setup</h2>

<p>The measured runs used:</p>

<table>
  <thead>
    <tr>
      <th>Setting</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bedrock model ID</td>
      <td><code class="language-plaintext highlighter-rouge">openai.gpt-5.5</code></td>
    </tr>
    <tr>
      <td>Bedrock endpoint</td>
      <td>Bedrock Mantle, <code class="language-plaintext highlighter-rouge">us-east-2</code></td>
    </tr>
    <tr>
      <td>OpenAI model ID</td>
      <td><code class="language-plaintext highlighter-rouge">gpt-5.5</code></td>
    </tr>
    <tr>
      <td>OpenAI endpoint</td>
      <td><code class="language-plaintext highlighter-rouge">https://api.openai.com/v1</code></td>
    </tr>
    <tr>
      <td>Reasoning efforts</td>
      <td><code class="language-plaintext highlighter-rouge">medium</code>, <code class="language-plaintext highlighter-rouge">high</code>, <code class="language-plaintext highlighter-rouge">xhigh</code></td>
    </tr>
    <tr>
      <td>Dataset</td>
      <td>GSM8K test split, 1,319 questions</td>
    </tr>
    <tr>
      <td>Concurrency</td>
      <td>1 (sequential)</td>
    </tr>
    <tr>
      <td>Max output tokens</td>
      <td>4,096</td>
    </tr>
    <tr>
      <td>Auth (Bedrock)</td>
      <td>AWS SigV4, <code class="language-plaintext highlighter-rouge">bedrock-mantle</code> service</td>
    </tr>
    <tr>
      <td>Auth (OpenAI)</td>
      <td>Bearer token via <code class="language-plaintext highlighter-rouge">OPENAI_API_KEY</code></td>
    </tr>
  </tbody>
</table>

<p>Setting Value Bedrock model ID <code class="language-plaintext highlighter-rouge">openai.gpt-5.5</code> Bedrock endpoint Bedrock Mantle, <code class="language-plaintext highlighter-rouge">us-east-2</code> OpenAI model ID <code class="language-plaintext highlighter-rouge">gpt-5.5</code> OpenAI endpoint <code class="language-plaintext highlighter-rouge">https://api.openai.com/v1</code> Reasoning efforts <code class="language-plaintext highlighter-rouge">medium</code>, <code class="language-plaintext highlighter-rouge">high</code>, <code class="language-plaintext highlighter-rouge">xhigh</code> Dataset GSM8K test split, 1,319 questions Concurrency 1 (sequential) Max output tokens 4,096 Auth (Bedrock) AWS SigV4, <code class="language-plaintext highlighter-rouge">bedrock-mantle</code> service Auth (OpenAI) Bearer token via <code class="language-plaintext highlighter-rouge">OPENAI_API_KEY</code></p>

<p>Sequential requests were a deliberate choice. Client-side concurrency is a real production variable, but it would have conflated provider queue effects with the underlying serving path. Running one request at a time isolated the thing we actually wanted to measure.</p>

<h2 id="bootstrap-the-environment">Bootstrap The Environment</h2>

<p>The repo uses <code class="language-plaintext highlighter-rouge">uv</code> for dependency management. Python 3.12 or later is required.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/LokaHQ/gpt-5-5-bedrock-openai-benchmark
<span class="nb">cd </span>gpt-5-5-bedrock-openai-benchmark
uv <span class="nb">sync</span>
</code></pre></div></div>

<p>This installs <code class="language-plaintext highlighter-rouge">httpx</code>, <code class="language-plaintext highlighter-rouge">boto3</code>, <code class="language-plaintext highlighter-rouge">python-dotenv</code>, and <code class="language-plaintext highlighter-rouge">tiktoken</code>. The dev group adds <code class="language-plaintext highlighter-rouge">pytest</code> and <code class="language-plaintext highlighter-rouge">ruff</code> if you also want the test suite and linter.</p>

<h2 id="configure-authentication">Configure Authentication</h2>

<p>The benchmark supports two authentication paths for Bedrock and one for OpenAI.</p>

<h3 id="bedrock-aws-sigv4">Bedrock: AWS SigV4</h3>

<p>This is the path used for the main runs. For a named profile:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">AWS_PROFILE</span><span class="o">=</span>your-bedrock-profile
</code></pre></div></div>

<p>The SigV4 signer is built in via <code class="language-plaintext highlighter-rouge">botocore</code>. No extra env vars are needed as long as the active profile has <code class="language-plaintext highlighter-rouge">bedrock-mantle:*</code> and <code class="language-plaintext highlighter-rouge">aws-marketplace:*</code> permissions.</p>

<p>To verify authentication without making model calls:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">AWS_PROFILE</span><span class="o">=</span>your-bedrock-profile uv run python scripts/benchmark_openai_latency.py <span class="se">\</span>
  <span class="nt">--provider</span> bedrock <span class="se">\</span>
  <span class="nt">--auth-debug</span>
</code></pre></div></div>

<p>This prints the auth mode, endpoint, and signed header names without touching the model. A clean output looks like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>auth_mode=sigv4
base_url=https://bedrock-mantle.us-east-2.api.aws/openai/v1
region=us-east-2
sigv4_service=bedrock-mantle
signed_header_names=authorization, content-type, host, x-amz-date, x-amz-security-token, x-client-request-id
</code></pre></div></div>

<h3 id="bedrock-bearer-token">Bedrock: Bearer Token</h3>

<p>If your setup authenticates with a Bedrock API key instead of AWS credentials, set it in <code class="language-plaintext highlighter-rouge">.env</code>. You can generate one in the Bedrock console under API keys (short-term keys last up to 12 hours), with the <code class="language-plaintext highlighter-rouge">aws-bedrock-token-generator package</code>, or for a long-term key via <code class="language-plaintext highlighter-rouge">aws iam create-service-specific-credential --service-name bedrock.amazonaws.com</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BEDROCK_OPENAI_API_KEY=your-bearer-token
</code></pre></div></div>

<h3 id="openai-api">OpenAI API</h3>

<p>Copy <code class="language-plaintext highlighter-rouge">.env.example</code> to <code class="language-plaintext highlighter-rouge">.env</code> and set your key:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cp</span> .env.example .env
<span class="c"># edit .env and set OPENAI_API_KEY=sk-...</span>
</code></pre></div></div>

<p>The .env file is excluded from version control. Never commit it.</p>

<h2 id="estimate-cost-before-running">Estimate Cost Before Running</h2>

<p>Before running against the full test split, use the cost estimator to know what you are committing to:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python scripts/estimate_openai_eval_cost.py <span class="se">\</span>
  <span class="nt">--dataset</span> gsm8k <span class="se">\</span>
  <span class="nt">--split</span> <span class="nb">test</span> <span class="se">\</span>
  <span class="nt">--model</span> gpt-5.5 <span class="se">\</span>
  <span class="nt">--visible-output-tokens</span> 256 <span class="se">\</span>
  <span class="nt">--reasoning-output-tokens</span> 256
</code></pre></div></div>

<p>The estimator downloads the GSM8K test split, counts input tokens with <code class="language-plaintext highlighter-rouge">tiktoken</code>, and outputs a table like:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Samples</th>
      <th>Avg input tok</th>
      <th>P95 input tok</th>
      <th>Output tok/sample</th>
      <th>Standard cost</th>
      <th>Batch cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gpt-5.5</code></td>
      <td>1,319</td>
      <td>93.5</td>
      <td>136</td>
      <td>512</td>
      <td>$20.88</td>
      <td>$10.44</td>
    </tr>
  </tbody>
</table>

<p>The <code class="language-plaintext highlighter-rouge">--reasoning-output-tokens</code> figure is an estimate. OpenAI bills reasoning tokens as output tokens even though they are not returned in API responses. The actual reasoning budget at <code class="language-plaintext highlighter-rouge">xhigh</code> is higher than <code class="language-plaintext highlighter-rouge">medium</code>, so this estimate is a lower bound for <code class="language-plaintext highlighter-rouge">xhigh</code> runs. The cost caps in the benchmark command give you a hard stop regardless.</p>

<h2 id="run-the-latency-benchmark">Run The Latency Benchmark</h2>

<h3 id="bedrock">Bedrock</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">AWS_PROFILE</span><span class="o">=</span>your-bedrock-profile uv run python scripts/benchmark_openai_latency.py <span class="se">\</span>
  <span class="nt">--provider</span> bedrock <span class="se">\</span>
  <span class="nt">--region</span> us-east-2 <span class="se">\</span>
  <span class="nt">--model</span> openai.gpt-5.5 <span class="se">\</span>
  <span class="nt">--dataset</span> gsm8k <span class="se">\</span>
  <span class="nt">--split</span> <span class="nb">test</span> <span class="se">\</span>
  <span class="nt">--warmups</span> 1 <span class="se">\</span>
  <span class="nt">--reasoning-effort</span> medium <span class="se">\</span>
  <span class="nt">--max-output-tokens</span> 4096 <span class="se">\</span>
  <span class="nt">--concurrency</span> 1 <span class="se">\</span>
  <span class="nt">--timeout-seconds</span> 240 <span class="se">\</span>
  <span class="nt">--max-billable-tokens</span> 750000 <span class="se">\</span>
  <span class="nt">--max-estimated-openai-cost-usd</span> 10.00 <span class="se">\</span>
  <span class="nt">--output-dir</span> result/gsm8k-gpt55-medium
</code></pre></div></div>

<h3 id="openai-api-1">OpenAI API</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python scripts/benchmark_openai_latency.py <span class="se">\</span>
  <span class="nt">--provider</span> openai <span class="se">\</span>
  <span class="nt">--model</span> gpt-5.5 <span class="se">\</span>
  <span class="nt">--dataset</span> gsm8k <span class="se">\</span>
  <span class="nt">--split</span> <span class="nb">test</span> <span class="se">\</span>
  <span class="nt">--warmups</span> 1 <span class="se">\</span>
  <span class="nt">--reasoning-effort</span> medium <span class="se">\</span>
  <span class="nt">--max-output-tokens</span> 4096 <span class="se">\</span>
  <span class="nt">--concurrency</span> 1 <span class="se">\</span>
  <span class="nt">--timeout-seconds</span> 240 <span class="se">\</span>
  <span class="nt">--max-billable-tokens</span> 750000 <span class="se">\</span>
  <span class="nt">--max-estimated-openai-cost-usd</span> 10.00 <span class="se">\</span>
  <span class="nt">--output-dir</span> result/openai-gsm8k-gpt55-medium
</code></pre></div></div>

<p>Change <code class="language-plaintext highlighter-rouge">--reasoning-effort</code> to <code class="language-plaintext highlighter-rouge">high</code> or <code class="language-plaintext highlighter-rouge">xhigh</code> for the other runs.</p>

<h3 id="what-the-six-main-commands-were">What the six main commands were</h3>

<p>The six benchmark runs that produced the numbers in the main post used these exact folder names:</p>

<table>
  <thead>
    <tr>
      <th>Effort</th>
      <th>Provider</th>
      <th>Output folder</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">medium</code></td>
      <td>Bedrock</td>
      <td><code class="language-plaintext highlighter-rouge">result/gsm8k-gpt55-medium</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">high</code></td>
      <td>Bedrock</td>
      <td><code class="language-plaintext highlighter-rouge">result/gsm8k-gpt55-high</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">xhigh</code></td>
      <td>Bedrock</td>
      <td><code class="language-plaintext highlighter-rouge">result/gsm8k-gpt55-xhigh</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">medium</code></td>
      <td>OpenAI</td>
      <td><code class="language-plaintext highlighter-rouge">result/openai-gsm8k-gpt55-medium</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">high</code></td>
      <td>OpenAI</td>
      <td><code class="language-plaintext highlighter-rouge">result/openai-gsm8k-gpt55-high</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">xhigh</code></td>
      <td>OpenAI</td>
      <td><code class="language-plaintext highlighter-rouge">result/openai-gsm8k-gpt55-xhigh-merged-clean</code></td>
    </tr>
  </tbody>
</table>

<p>The OpenAI <code class="language-plaintext highlighter-rouge">xhigh</code> folder name ends in <code class="language-plaintext highlighter-rouge">-merged-clean</code> because the original run hit a client-side DNS outage near the end — 181 requests failed to resolve on our own network, not on the provider’s. We retried those separately and merged the clean records back into a single JSONL before scoring, so the merged result shows 1,319 / 0 — all prompts answered, no failures — which is the number reported in the main post.</p>

<p>That DNS outage was on our side and counts against neither provider. The genuine provider-side errors showed up elsewhere: the OpenAI <code class="language-plaintext highlighter-rouge">medium</code> and <code class="language-plaintext highlighter-rouge">high</code> runs each returned 5xx responses from OpenAI’s edge (2× HTTP 503, 1× HTTP 520), while Bedrock returned zero errors across all 3,957 requests at every effort level.</p>

<h2 id="what-the-harness-does-during-a-run">What the Harness Does During a Run</h2>

<p>Each request goes through <code class="language-plaintext highlighter-rouge">run_once()</code>, which opens an <code class="language-plaintext highlighter-rouge">httpx</code> streaming connection and measures three timestamps:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">TTFB</code> (time to first byte): when the first non-empty line arrives from the server. This is infrastructure-only: connection setup, auth, admission, routing.</li>
  <li><code class="language-plaintext highlighter-rouge">TTFT</code> (time to first token): when the first <code class="language-plaintext highlighter-rouge">response.output_text.delta</code> event arrives. This includes model reasoning before the first output character is produced.</li>
  <li>total latency: when the stream closes after <code class="language-plaintext highlighter-rouge">[DONE]</code></li>
</ul>

<p>Token usage, input, output, and reasoning separately, comes from the <code class="language-plaintext highlighter-rouge">usage</code> block in the final <code class="language-plaintext highlighter-rouge">response.done</code> event. Output throughput is derived as:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>output_tokens / total_seconds
</code></pre></div></div>

<p>Each completed or failed record is written immediately to a partial JSONL file, so a mid-run crash does not destroy the entire sample.</p>

<p>The live progress line looks like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bedrock openai.gpt-5.5 run 142/1320: ok; in=87 out=44 reasoning=312; cost=$0.003456; cumulative_tokens=18,432; cumulative_est_openai_cost=$0.4891
</code></pre></div></div>

<p>The cost caps (<code class="language-plaintext highlighter-rouge">--max-billable-tokens</code>, <code class="language-plaintext highlighter-rouge">--max-estimated-openai-cost-usd</code>) check after each completed request and stop the run cleanly if either threshold is crossed.</p>

<h2 id="output-files">Output Files</h2>

<p>Each run writes three files into the output directory:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>result/gsm8k-gpt55-medium/
├── openai_responses_latency_20260610T083101Z.jsonl
├── openai_responses_latency_20260610T083101Z_summary.json
└── openai_responses_latency_20260610T083101Z.md
</code></pre></div></div>

<p>The JSONL file has one flat record per request:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bedrock"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"openai.gpt-5.5"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"run_index"</span><span class="p">:</span><span class="w"> </span><span class="mi">142</span><span class="p">,</span><span class="w">
  </span><span class="nl">"prompt_index"</span><span class="p">:</span><span class="w"> </span><span class="mi">141</span><span class="p">,</span><span class="w">
  </span><span class="nl">"prompt_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gsm8k-test-141"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"started_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-06-10T08:52:13.441Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ok"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ttfb_ms"</span><span class="p">:</span><span class="w"> </span><span class="mf">218.4</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ttft_ms"</span><span class="p">:</span><span class="w"> </span><span class="mf">1847.2</span><span class="p">,</span><span class="w">
  </span><span class="nl">"total_ms"</span><span class="p">:</span><span class="w"> </span><span class="mf">2613.9</span><span class="p">,</span><span class="w">
  </span><span class="nl">"input_tokens"</span><span class="p">:</span><span class="w"> </span><span class="mi">87</span><span class="p">,</span><span class="w">
  </span><span class="nl">"output_tokens"</span><span class="p">:</span><span class="w"> </span><span class="mi">44</span><span class="p">,</span><span class="w">
  </span><span class="nl">"reasoning_tokens"</span><span class="p">:</span><span class="w"> </span><span class="mi">312</span><span class="p">,</span><span class="w">
  </span><span class="nl">"output_tokens_per_second"</span><span class="p">:</span><span class="w"> </span><span class="mf">50.2</span><span class="p">,</span><span class="w">
  </span><span class="nl">"output_text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The total is 14 apples.</span><span class="se">\n\n</span><span class="s2">#### 14"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expected_answer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"14 apples.</span><span class="se">\n\n</span><span class="s2">#### 14"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"estimated_openai_cost_usd"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.003456</span><span class="p">,</span><span class="w">
  </span><span class="nl">"request_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"error"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The summary JSON contains the LLMPerf-style aggregate statistics: full distributions (p25, p50, p75, p90, p95, p99, mean, min, max, stddev) for each metric across all successful requests.</p>

<p>The Markdown file is the same summary in readable form, including a failure log if any requests errored.</p>

<h2 id="score-accuracy-offline">Score Accuracy Offline</h2>

<p>The accuracy scorer is a separate script that never touches the API. It works entirely from the saved JSONL files, which is important: you can re-score a run with an updated normalization rule without paying for another API call.</p>

<p>To score all six main runs at once:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python scripts/score_gsm8k_accuracy.py <span class="nt">--main-runs</span>
</code></pre></div></div>

<p>This downloads the original GSM8K test split from GitHub, extracts the <code class="language-plaintext highlighter-rouge">#### &lt;answer&gt;</code> line from both the reference and the saved model response, normalizes both (strips commas, dollar signs, trailing periods, handles Unicode minus), and compares them. The output goes into each run’s folder plus an aggregate comparison:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>result/
├── gsm8k_accuracy_main_runs.json
├── gsm8k_accuracy_main_runs.md
├── gsm8k-gpt55-medium/
│   ├── gsm8k_accuracy_summary.json
│   └── gsm8k_accuracy_summary.md
└── ...
</code></pre></div></div>

<p>To score a single run, pass <code class="language-plaintext highlighter-rouge">--benchmark-jsonl</code> and optionally <code class="language-plaintext highlighter-rouge">--output-dir</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python scripts/score_gsm8k_accuracy.py <span class="se">\</span>
  <span class="nt">--benchmark-jsonl</span> result/gsm8k-gpt55-medium/openai_responses_latency_20260610T083101Z.jsonl
</code></pre></div></div>

<p>The normalization logic handles most of the common failure modes: a model writing <code class="language-plaintext highlighter-rouge">$14.00</code> instead of <code class="language-plaintext highlighter-rouge">14</code>, writing <code class="language-plaintext highlighter-rouge">14,000</code> with a comma, or using a Unicode minus. The scorer extracts the last <code class="language-plaintext highlighter-rouge">####</code> match in the output, so reasoning steps that mention intermediate numbers do not pollute the answer comparison.</p>

<h2 id="the-normalization-detail-that-matters">The Normalization Detail That Matters</h2>

<p>The answer extractor uses the <code class="language-plaintext highlighter-rouge">####</code> prefix that GSM8K itself defines. Both the reference answers from the dataset and the model answers from the benchmark follow the same format because the benchmark prompt asks the model to use it:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Solve this grade-school math problem. Show concise reasoning,
then put the final numeric answer on a separate line prefixed
with ####.
</code></pre></div></div>

<p>This makes the scorer deterministic and fast. The only edge cases are models that skip the <code class="language-plaintext highlighter-rouge">####</code> prefix entirely (scored as <code class="language-plaintext highlighter-rouge">missing_model_answer</code>) or produce a format the normalizer cannot parse to a canonical number. In practice, GPT-5.5 followed the format consistently across all 1,319 questions in every run.</p>

<h2 id="running-at-reduced-scale">Running At Reduced Scale</h2>

<p>Before committing to the full 1,319-question run, use <code class="language-plaintext highlighter-rouge">--limit</code> to test against a small slice:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">AWS_PROFILE</span><span class="o">=</span>your-bedrock-profile uv run python scripts/benchmark_openai_latency.py <span class="se">\</span>
  <span class="nt">--provider</span> bedrock <span class="se">\</span>
  <span class="nt">--model</span> openai.gpt-5.5 <span class="se">\</span>
  <span class="nt">--dataset</span> gsm8k <span class="se">\</span>
  <span class="nt">--split</span> <span class="nb">test</span> <span class="se">\</span>
  <span class="nt">--limit</span> 20 <span class="se">\</span>
  <span class="nt">--reasoning-effort</span> medium <span class="se">\</span>
  <span class="nt">--max-output-tokens</span> 4096 <span class="se">\</span>
  <span class="nt">--output-dir</span> result/smoke-test
</code></pre></div></div>

<p>This runs the first 20 questions, produces all the same output files, and lets you verify auth, output format, and metric collection before spending the full budget.</p>

<h2 id="caveats">Caveats</h2>

<p>This benchmark is intentionally narrow:</p>

<ul>
  <li>
    <p>It measures sequential single-stream requests from one client location. Concurrent-request behavior and multi-region comparisons are out of scope.</p>
  </li>
  <li>
    <p>The dataset is grade-school math. Short math prompts with compact answers are not representative of all enterprise workloads. Longer prompts or longer outputs would shift the TTFT and throughput numbers.</p>
  </li>
  <li>
    <p>Latency and error rates are point-in-time properties of a serving fleet. Bedrock’s zero-error rate and lower TTFB reflect the fleet as it stood in June 2026, weeks after the Bedrock launch. Both can change as load grows.</p>
  </li>
  <li>
    <p>Cost estimates use OpenAI’s published list prices as of June 2026. Bedrock list prices differ slightly; the main post notes the specific numbers.</p>
  </li>
</ul>

<p>Those caveats do not change the main result: same model, same answer quality, faster serving on Bedrock with zero errors across nearly 4,000 requests. But they do matter when you translate these numbers to a production SLO.</p>

<h2 id="takeaway">Takeaway</h2>

<p>The useful part is not only the latency numbers. It is the measurement discipline.</p>

<p>The harness records every request, separates warmup from measured runs, writes partial results during the run, and scores accuracy offline from saved artifacts. That structure makes the comparison clean and reproducible. You can rerun against your own region, your own workload shape, and your own concurrency level and get numbers that mean something for your system, not just ours.</p>

<p>For teams evaluating OpenAI on Bedrock against the first-party API, that is the starting point: run the benchmark against your own traffic shape before committing an SLO to results from someone else’s setup.</p>]]></content><author><name>shekswess</name></author><category term="AI" /><category term="LLM" /><category term="OpenAI" /><category term="Amazon Bedrock" /><category term="GPT-5.5" /><category term="Benchmarking" /><category term="GSM8K" /><category term="AWS" /><category term="Latency" /><summary type="html"><![CDATA[A practical engineering walkthrough for reproducing Loka's GPT-5.5 benchmark across Amazon Bedrock and the OpenAI API.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://lokahq.github.io/gpt-bedrock-openai-benchmark/blog/assets/cover-banner-gpt.png" /><media:content medium="image" url="https://lokahq.github.io/gpt-bedrock-openai-benchmark/blog/assets/cover-banner-gpt.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How Loka Evaluates and Builds with Frontier Models on AWS</title><link href="https://shekswess.github.io/how-loka-evaluates-and-builds-with-frontier-models-on-aws.html" rel="alternate" type="text/html" title="How Loka Evaluates and Builds with Frontier Models on AWS" /><published>2026-06-14T16:00:00+00:00</published><updated>2026-06-14T16:00:00+00:00</updated><id>https://shekswess.github.io/how-loka-evaluates-and-builds-with-frontier-models-on-aws</id><content type="html" xml:base="https://shekswess.github.io/how-loka-evaluates-and-builds-with-frontier-models-on-aws.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>OpenAI <strong>GPT-5.5, GPT-5.4, and Codex</strong> are now generally
available on <strong>Amazon Bedrock</strong>. For enterprises, that is more than
a new catalog entry. It creates a path to use OpenAI capabilities inside the AWS
security, governance, procurement, and operational controls they already run.
Loka is one of only five OpenAI launch partners globally, giving us a front-row
role in helping customers turn this new path into production systems.</p>

<p>That designation is both a milestone and a practical signal. Since 2023, Loka
teams have helped lead more than 500 GenAI and agentic AI launches, with over half
already in production or moving there. OpenAI on Bedrock gives that work a cleaner
route from experimentation to secure, scalable enterprise systems in the AWS
environments customers already trust.</p>

<p>A launch is easy to celebrate and hard to substantiate. So this post covers
two things:</p>

<ol>
  <li><strong>A benchmark: GPT-5.5 on Bedrock vs. the native OpenAI API.</strong>
Before we built anything, we asked the foundational question: does routing
GPT-5.5 through Amazon Bedrock change the model you get? We ran the full
GSM8K test split (1,319 questions) through both provider paths at three
reasoning levels and compared accuracy, latency, throughput, and reliability
head to head. The benchmark confirmed the same answer quality, faster
serving, and zero failures on Bedrock.</li>
  <li><strong>Two regulated-industry copilots, built on the path that passed.</strong>
Using Loka’s production agent architecture and delivery patterns, we stood up
the <strong>FSI Evidence Copilot</strong> (financial services: evidence
packets for small-business lending, KYC refresh, and AML alert review) and
the <strong>Clinical Evidence Copilot</strong> (healthcare &amp; life
sciences: synthetic-patient evidence packets for coverage and medication
reviews). The point is not to expose implementation mechanics; it is to show
how authentication, scoped data access, observability, guardrails,
deployment, and human review can travel across use cases.</li>
</ol>

<p>They share production obligations: governed access, traceability, guardrails,
deployment discipline, and, most importantly, a human-review boundary.
<strong>Both prepare evidence for a human; neither makes the decision.</strong>
The domain services and data sources differ by use case, but the validated
model path is the same: OpenAI on Bedrock, benchmarked before it was trusted
with production workloads.</p>

<p>For the engineering walkthrough behind the benchmark harness, commands,
result files, and scoring logic, read our companion Medium post:
<a href="https://medium.com/p/3d57899fc058">Benchmarking GPT-5.5 on
Amazon Bedrock vs. the OpenAI API</a>.</p>

<p>What we want you to take away. The useful unlock is the
combination of a model-agnostic production architecture, Bedrock’s governance
surface, and OpenAI-compatible APIs, all verified to be production-grade
before the first line of copilot code was written. Together, they let teams
move regulated workloads from prototype to production while preserving the AWS
controls their security and platform teams already understand.</p>

<h2 id="why-openai-on-bedrock-matters-for-the-enterprise">Why OpenAI on Bedrock matters for the enterprise</h2>

<p>For enterprises already running regulated workloads on AWS, the biggest obstacle
is often the operational change that follows when data, access controls, audit
trails, procurement terms, and procedures move outside the approved cloud
environment. A new model vendor can trigger security review, legal review,
data-processing review, vendor onboarding, logging changes, and new runbooks.
OpenAI models on Bedrock make the shift materially smaller because the model
choice sits inside the AWS control plane teams already trust:</p>

<table>
  <thead>
    <tr>
      <th>Concern</th>
      <th>What Bedrock gives you</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Data residency &amp; privacy</td>
      <td>Inference runs in the selected Bedrock Region; prompts and responses are not used to train models or shared with model providers.</td>
    </tr>
    <tr>
      <td>Identity &amp; access</td>
      <td>Standard AWS controls such as IAM, VPC connectivity, and CloudTrail around the model invocation path.</td>
    </tr>
    <tr>
      <td>Safety</td>
      <td>Amazon Bedrock Guardrails and platform controls can be applied through supported Bedrock APIs and model paths.</td>
    </tr>
    <tr>
      <td>API fit &amp; portability</td>
      <td>Use AWS-native APIs such as <code class="language-plaintext highlighter-rouge">Converse</code> for supported models, or OpenAI-compatible <code class="language-plaintext highlighter-rouge">Responses</code> and chat-completions APIs on Bedrock when that is the better fit.</td>
    </tr>
  </tbody>
</table>

<p>That last row is the one that compounds. For existing Bedrock-native workloads,
<code class="language-plaintext highlighter-rouge">Converse</code> keeps a common interface across supported model families.
For GPT-5.5, GPT-5.4, and Codex on Bedrock, AWS points developers to the
OpenAI-compatible <code class="language-plaintext highlighter-rouge">Responses</code> API on the Bedrock Mantle endpoint. The
exact adapter can change by workload, while the architecture stays steady: model
selection belongs in configuration, while auth, evidence access, auditability,
streaming, and deployment stay stable.</p>

<h3 id="capability-is-part-of-the-enterprise-case">Capability is part of the enterprise case</h3>

<p>The governance story only matters if the models are worth adopting. Today, the
evidence is strongest in agentic software work: on Datacurve’s DeepSWE benchmark,
which measures long-horizon repository tasks, GPT-5.5 leads the published sweep
while using fewer output tokens than Opus 4.8 at comparable and higher reasoning
levels. Opus 4.8 improves as reasoning effort rises and has strong results too,
but its max configuration spends far more output tokens and time for a lower
pass@1 than GPT-5.5 xhigh. The enterprise takeaway is that the Bedrock launch
brings one of the current frontier leaders into the AWS operating model teams
already use.</p>

<p>Anthropic has since released Claude Fable 5, the first
of its Claude 5 family and a tier in positions above Opus. It is new enough that
the published sweeps in this post do not yet include it, and nothing in this
post turns on it. Since our comparison is about provider paths for models of comparable weights,
and not whatever lab holds the leaderboard for best model, we decided not to include it here.</p>

<p><img src="../assets/images/openai/image.png" alt="openai" /></p>

<p>DeepSWE snapshot from Datacurve, updated May 30, 2026. X axis is mean output tokens per task; Y axis is pass@1. Each point is labeled by reasoning level. Tooltips include cost and time per task.</p>

<p>Codex adds the adoption signal behind the benchmark story. OpenAI reports that
more than 5 million people use Codex every week, with non-developers now making up
about 20% of users and growing more than three times as fast as developers. That
matters for enterprises because the same agentic pattern is moving from coding into
analysis, reporting, workflow automation, and other knowledge work, exactly the
kind of operational surface where AWS governance and procurement matter.</p>

<h2 id="benchmarking-gpt-55-on-bedrock-against-the-openai-api">Benchmarking GPT-5.5 on Bedrock against the OpenAI API</h2>

<p>Capability is only half the enterprise question. The other half is whether the
governed model path costs you anything in accuracy, in the latency users feel,
or in reliability. So we measured it. We ran the full <strong>GSM8K test split
(1,319 questions)</strong> through both provider paths, with
<code class="language-plaintext highlighter-rouge">openai.gpt-5.5</code> on Amazon Bedrock Mantle in <code class="language-plaintext highlighter-rouge">us-east-2</code>
and <code class="language-plaintext highlighter-rouge">gpt-5.5</code> on the OpenAI first-party API, at three reasoning
efforts (<em>medium</em>, <em>high</em>, <em>xhigh</em>). Same prompts, same
OpenAI-compatible <code class="language-plaintext highlighter-rouge">Responses</code> shape, same output caps, same streaming
metric definitions, and sequential requests so client-side concurrency never
pollutes the numbers. That setup answers two questions at once. Does quality
survive the provider change, and which path is faster in practice?</p>

<h3 id="accuracy-survives-the-provider-change">Accuracy survives the provider change</h3>

<table>
  <thead>
    <tr>
      <th>Reasoning effort</th>
      <th>Bedrock <code class="language-plaintext highlighter-rouge">openai.gpt-5.5</code></th>
      <th>OpenAI <code class="language-plaintext highlighter-rouge">gpt-5.5</code></th>
      <th>Delta</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>medium</td>
      <td>97.57%</td>
      <td>97.42%</td>
      <td>−0.15 pp</td>
    </tr>
    <tr>
      <td>high</td>
      <td>97.57%</td>
      <td>97.35%</td>
      <td>−0.22 pp</td>
    </tr>
    <tr>
      <td>xhigh</td>
      <td>97.42%</td>
      <td>97.35%</td>
      <td>−0.07 pp</td>
    </tr>
  </tbody>
</table>

<p>Bedrock edges ahead in all three runs, but the honest read is parity. The gaps
are one to three questions out of 1,319, well within run-to-run noise for a
sampled model. That is the result an enterprise team wants to see. It says the
weights behind both endpoints behave the same, so moving the workload onto the
governed AWS path costs nothing in answer quality, and everything that
<em>does</em> differ between the two paths is serving infrastructure.</p>

<h3 id="bedrock-is-faster-where-it-matters">Bedrock is faster where it matters</h3>

<table>
  <thead>
    <tr>
      <th>Effort</th>
      <th>Provider</th>
      <th>TTFB p50</th>
      <th>TTFT p50</th>
      <th>Total p50</th>
      <th>Total p90</th>
      <th>Mean tok/s</th>
      <th>Failed</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>medium</td>
      <td>Bedrock</td>
      <td>220 ms</td>
      <td>1,939 ms</td>
      <td>2,881 ms</td>
      <td>4,369 ms</td>
      <td>47.4</td>
      <td>0 / 1,319</td>
    </tr>
    <tr>
      <td>medium</td>
      <td>OpenAI</td>
      <td>484 ms</td>
      <td>1,956 ms</td>
      <td>2,859 ms</td>
      <td>4,908 ms</td>
      <td>39.1</td>
      <td>2 / 1,319</td>
    </tr>
    <tr>
      <td>high</td>
      <td>Bedrock</td>
      <td>224 ms</td>
      <td>2,662 ms</td>
      <td>3,455 ms</td>
      <td>4,801 ms</td>
      <td>48.5</td>
      <td>0 / 1,319</td>
    </tr>
    <tr>
      <td>high</td>
      <td>OpenAI</td>
      <td>497 ms</td>
      <td>2,356 ms</td>
      <td>3,248 ms</td>
      <td>5,712 ms</td>
      <td>41.3</td>
      <td>1 / 1,319</td>
    </tr>
    <tr>
      <td>xhigh</td>
      <td>Bedrock</td>
      <td>223 ms</td>
      <td>3,250 ms</td>
      <td>3,803 ms</td>
      <td>5,792 ms</td>
      <td>51.8</td>
      <td>0 / 1,319</td>
    </tr>
    <tr>
      <td>xhigh</td>
      <td>OpenAI</td>
      <td>502 ms</td>
      <td>3,280 ms</td>
      <td>4,334 ms</td>
      <td>8,273 ms</td>
      <td>39.7</td>
      <td>retried*</td>
    </tr>
  </tbody>
</table>

<p>*The OpenAI xhigh run needed failed requests retried to complete all 1,319
samples; the published numbers come from the merged clean run. Bedrock completed
all 3,957 requests across the three efforts with zero errors.</p>

<p><img src="../assets/images/openai/image_1.png" alt="openai" /></p>

<p>Time to first byte (p50) by reasoning effort. TTFB is measured before any token
is generated, so this is purely the API path. Bedrock answers in ~220 ms at
every effort level, and the OpenAI API takes about 2.2× longer.</p>

<p>End-to-end streamed latency, p50 (solid) and p90 (translucent). Medians are
close at medium and high; the gap opens at xhigh and at the tails, where
Bedrock’s p90 stays 0.5–2.5 s ahead.</p>

<p>Mean per-request output throughput. Bedrock decodes 20–30% faster at every
effort level, and the gap widens as reasoning effort (and output length) grows.</p>

<p>The medians deserve an honest read. At medium and high effort the
<em>median</em> end-to-end latencies are effectively tied (the Bedrock high run
actually spent slightly more reasoning tokens, which costs it the p50 comparison
there). But every other dimension leans one way. Time to first byte is ~220 ms
on Bedrock versus ~480–500 ms on the OpenAI API at every effort level. Mean
output throughput is 20–30% higher on Bedrock. The tail percentiles (p90/p95)
favor Bedrock in all three runs and the gap grows with effort. At xhigh, p95 is
9.2 s on Bedrock versus 11.4 s on OpenAI. Reliability went one way only. Bedrock
finished nearly 4,000 calls without a single failure, while every OpenAI run had
errored requests, including stalled outliers that ran past 100 seconds.</p>

<h3 id="why-is-bedrock-faster-our-three-cents">Why is Bedrock faster? Our three cents</h3>

<p>The accuracy parity above is what makes the latency question interesting. If the
model is the same, the difference is the serving path. Three observations from
the data, offered as informed interpretation rather than inside knowledge:</p>

<ul>
  <li><strong>The TTFB gap is infrastructure, not the model.</strong> Time to first
byte is measured before a single token is generated, so it captures connection
setup, authentication, admission, and routing. Bedrock’s regional endpoint held
a flat ~220 ms p50 across all three runs regardless of effort; the OpenAI API hovered
around ~480–500 ms with a much heavier tail (p99 of ~3 s, worst cases over
100 s). That is the difference between a dedicated regional AWS endpoint and a
globally shared API front door absorbing the world’s GPT-5.5 traffic.</li>
  <li><strong>OpenAI’s throughput looks paced; Bedrock’s looks unconstrained.</strong>
Across ~3,950 OpenAI requests, per-request decode speed clustered tightly
(stddev ~9.5 tok/s) and <em>never once exceeded ~67 tok/s</em>. The observed
maximum was 66.2, 66.7, and 66.7 tok/s in the three runs, which looks like a
deliberate per-request ceiling or aggressive batching on a heavily multi-tenant
fleet. Bedrock requests ranged up to ~108 tok/s with means of 47–52. Consistent
with newer, less-contended capacity that is not yet rationing decode speed.</li>
  <li><strong>Demand asymmetry, for now.</strong> The first-party API serves the
bulk of global GPT-5.5 demand; the Bedrock endpoint launched weeks ago. Lower
utilization means shorter queues, faster admission, and tighter tails, which is
exactly the shape of the data, including Bedrock’s zero error rate. The honest
caveat is that this part of the advantage can narrow as Bedrock adoption grows.</li>
</ul>

<p>There is also a hardware angle. Amazon’s
<a href="https://www.aboutamazon.com/news/aws/amazon-open-ai-strategic-partnership-investment">$50 billion investment in OpenAI</a>
comes with a commitment of
<a href="https://www.tomshardware.com/tech-industry/amazon-invests-50-billion-in-openai">roughly 2 gigawatts of Trainium capacity</a>,
and in March AWS and Cerebras
<a href="https://press.aboutamazon.com/aws/2026/3/aws-and-cerebras-collaboration-aims-to-set-a-new-standard-for-ai-inference-speed-and-performance-in-the-cloud">announced a serving architecture</a>
that splits inference across purpose-built silicon, with Trainium handling the
parallel prefill phase and Cerebras wafer-scale chips handling the
bandwidth-hungry decode phase. AWS is pitching it as “the fastest AI inference
available through Amazon Bedrock.” We don’t know what hardware served our
requests, since neither company says. The only public speed figure for OpenAI on
Cerebras, at
<a href="https://openai.com/index/introducing-gpt-5-3-codex-spark/">over 1,000 tokens per second</a>,
comes from a much smaller model built specifically for that hardware, so it
says little about what GPT-5.5 would do there. The more useful signal is AWS’s
own framing. The announcement describes the new stack as launching months later
and as “an order of magnitude faster” than what Bedrock serves today. A 20–30%
throughput edge is not an order of magnitude, so whatever served our requests,
the new silicon doesn’t explain the gap we measured. What it does tell you is
where this is going. AWS is building Bedrock to be the fastest place to run
these models, and the advantage we measured is probably the early version of it.</p>

<p>Independent corroboration. We are not the only ones measuring
this. Artificial
Analysis’s GPT-5.5 provider comparison reaches the same conclusion on a
different workload, measuring Bedrock at 52.3 output tok/s versus OpenAI at
50.5, with materially lower end-to-end latency on their xhigh measurement
(~76 s vs ~100 s to first answer token), summarized on their page as “Amazon
offers the best performance with both the highest speed and lowest latency.”
Their
medium-effort page
shows the same shape, 53.8 vs 48.3 tok/s and 7.9 s vs 9.5 s to first answer
token, independently matching the direction of every latency and throughput
number we measured. The flip side is also theirs. OpenAI’s list price is
slightly lower ($5.00/$30.00 vs $5.50/$33.00 per 1M input/output tokens), so
the trade is roughly 10% on price for the faster, more reliable path that also
lives inside AWS governance.</p>

<p><img src="../assets/images/openai/image_2.png" alt="openai" /></p>

<p>Our six GSM8K runs as a quadrant, with median end-to-end latency against mean
output throughput and both axes starting at zero. Points are labeled by
reasoning effort. Up and to the left is the good corner. Bedrock streams
faster at every effort, while the latency medians stay close at medium and
high and separate at xhigh.</p>

<p>Scope of these numbers. One model (GPT-5.5), one Bedrock Region
(us-east-2), one client location, sequential single-stream
requests, short math prompts, measured in June 2026. Latency and error rates are
point-in-time properties of a serving fleet, not constants. Rerun against your
own region, workload shape, and concurrency before committing an SLO to these.</p>

<h2 id="production-service-pattern">Production service pattern</h2>

<p>That was the measurement half of this post. The benchmark validated the model
path, with the same answer quality, faster serving, and zero failures on
Bedrock, so from here on we switch from measuring to building: everything below
is about the two copilots we shipped on that path.</p>

<h3 id="what-gets-easier-with-openai-on-bedrock">What gets easier with OpenAI on Bedrock</h3>

<p>In the examples below, the announcement matters because teams can pair OpenAI’s
strongest reasoning and agentic models with the AWS controls that already govern
regulated workloads:</p>

<table>
  <thead>
    <tr>
      <th>Build pressure</th>
      <th>Why OpenAI on Bedrock helps</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Hard reasoning over messy evidence</td>
      <td>GPT-5.5 and GPT-5.4 are available for complex professional and agentic work, so the same review workflow can use stronger models without leaving AWS.</td>
    </tr>
    <tr>
      <td>Security and platform review</td>
      <td>Inference stays in the selected Bedrock Region and runs under AWS controls like IAM, PrivateLink/VPC isolation, KMS encryption, and CloudTrail logging.</td>
    </tr>
    <tr>
      <td>Procurement and scale-up</td>
      <td>Usage can count through existing AWS commitments, which removes a common blocker between prototype approval and production rollout.</td>
    </tr>
    <tr>
      <td>Engineering velocity</td>
      <td>The OpenAI-compatible <code class="language-plaintext highlighter-rouge">Responses</code> API lets teams adapt existing OpenAI patterns while keeping deployment, auth, observability, and data access on AWS.</td>
    </tr>
  </tbody>
</table>

<h3 id="one-governed-pattern-two-copilots">One governed pattern, two copilots</h3>

<p>The healthcare and financial-services demos share something more useful than a
shared implementation: a governed service pattern. Each one starts with
authenticated access, retrieves only scoped evidence, maps that evidence to policy,
labels gaps and stale records, emits observable traces and artifacts, applies
guardrails around scope, and leaves the final decision with a qualified human.</p>

<p><img src="../assets/images/openai/image_3.png" alt="openai" /></p>

<p>The two copilots use the same production service pattern across different
domains. Each retrieves scoped evidence, turns it into a cited draft packet, and
hands the decision back to a human reviewer.</p>

<p>This is the useful kind of reuse. The healthcare and financial-services examples
read different sources and produce different review artifacts because the work is
different. What stays the same is the operating contract: cite the evidence, expose
gaps, preserve auditability, keep the run observable, and refuse the final call.</p>

<p>OpenAI on Bedrock matters here because the model can change without moving the
workflow out of the governed AWS environment. For GPT-5.5, GPT-5.4, and Codex,
AWS points developers to the OpenAI-compatible <code class="language-plaintext highlighter-rouge">Responses</code> API on
Bedrock Mantle. The adapter changes; the domain services, source IDs, auth boundary,
audit trail, and human-review output contract should stay intact.</p>

<p>That also makes model migration more practical. Teams already running Bedrock
workloads on Anthropic, Amazon Nova, or other supported model families can add
OpenAI where it fits without rebuilding the surrounding product. The work is still
an engineering migration, not a string replacement: model IDs, invocation adapters,
streaming behavior, tool contracts, observability hooks, and regression checks all
need attention. But the business workflow, AWS security posture, and production
service boundary can remain recognizable.</p>

<h2 id="financial-services-the-fsi-evidence-copilot">Financial services: the FSI Evidence Copilot</h2>

<p>We lead with financial services because the cost of a wrong call is unambiguous.
Lending, KYC, and AML review are evidence-assembly problems wearing a decision’s
clothing: an analyst has to find the policy, locate every qualifying fact across
statements and alerts, decide what is missing, and document the chain before any
human judgment is even possible. The FSI Evidence Copilot does the assembly and,
critically, never approves credit, files a SAR, closes an alert, or freezes funds.</p>

<table>
  <thead>
    <tr>
      <th>Question</th>
      <th>Answer</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>What is manual today?</td>
      <td>An analyst hunts across bank statements, case notes, policies, and alerts before the real review can begin.</td>
    </tr>
    <tr>
      <td>What does the copilot do?</td>
      <td>It assembles a requirement-by-requirement evidence packet with source IDs, missing items, and stale documents called out.</td>
    </tr>
    <tr>
      <td>Where does the human stay in control?</td>
      <td>The analyst still approves, denies, escalates, files, or closes. The agent only prepares the packet.</td>
    </tr>
    <tr>
      <td>What changes operationally?</td>
      <td>Less prep time, fewer uncited claims, and a clearer audit trail for every exception.</td>
    </tr>
  </tbody>
</table>

<p><br /></p>

<table>
  <thead>
    <tr>
      <th>Tool</th>
      <th>What it does</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fsi_list_cases</code> / <code class="language-plaintext highlighter-rouge">fsi_get_case_brief</code></td>
      <td>Enumerate synthetic cases and pull case facts with source IDs.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fsi_map_policy_evidence</code></td>
      <td>Map each policy requirement to evidence, with explicit gap and staleness flags.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fsi_create_chart_artifact</code></td>
      <td>Generate cash-flow / risk chart artifacts for the packet.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fsi_draft_review_packet</code></td>
      <td>Assemble the draft, human-review-only packet.</td>
    </tr>
  </tbody>
</table>

<h3 id="walkthrough-small-business-credit-renewal-atlas-industrial-supplies">Walkthrough: small-business credit renewal (Atlas Industrial Supplies)</h3>

<p>Atlas, a regional industrial-parts distributor, asks to raise its credit line from
$450K to $700K. The copilot opens the case, maps the <em>Small Business Credit
Policy 2026</em>, and immediately sorts the file into supported evidence and open
gaps:</p>

<p>FSI Evidence Copilot · SBL-2026-0417-ATLAS: evidence checklist (excerpt)</p>

<p>RequirementStatusEvidence</p>

<p><img src="../assets/images/openai/image_4.png" alt="openai" /></p>

<p>The first graph earns its keep by putting the underwriting exception in one
glance. The policy says credit increases above
$500K need DSCR at or above 1.25×. Atlas is below that floor in four of the last
six months, with a January trough at 1.11× and an April reading of 1.08×. Instead
of producing an automated denial, the copilot gives the analyst the evidence
behind a request that cannot be rubber-stamped.</p>

<p><img src="../assets/images/openai/image_5.png" alt="openai" /></p>

<p>Atlas monthly DSCR vs. the 1.25× policy threshold (dashed). Source: DOC-ATLAS-BANK-APR.</p>

<p>The second graph needs more context, because wire counts alone cannot prove
suspicious activity. We include it because it shows the same review pattern in a
different financial-services workflow: the agent spots a jump, attaches it to the
source alert, and explains why a human should look. Atlas jumps to 17 outgoing
wires in April against a five-month average of seven. Northstar, an AML case,
spikes to 19 against an expected five. The copilot routes the anomaly into the
review packet for a human to evaluate.</p>

<p><img src="../assets/images/openai/image_6.png" alt="openai" /></p>

<p>Monthly outgoing wire count. The chart gives reviewers a triage cue: the copilot surfaces the spike, cites the source alert, and routes the anomaly to a human reviewer.</p>

<p>When asked to just approve the increase, the copilot refuses the decision and offers the next analyst action instead:</p>

<p>Out of bounds: declined</p>

<p>“I can’t approve or deny this credit increase. That’s a human underwriting
decision. The DSCR exception (1.18× vs. 1.25×) and the 412-day-old ownership
attestation both require review before any recommendation.”</p>

<p>In bounds: delivered</p>

<p>Draft packet with executive snapshot, requirement-to-evidence map, three
ranked risk indicators, and recommended next actions: request AR aging and a
refreshed attestation, explain the April wire spike, route the DSCR exception
to underwriting.</p>

<p><img src="../assets/images/openai/image_7.png" alt="openai" /></p>

<figure class="post-video">
<video controls="" preload="metadata" poster="https://lokahq.github.io/gpt-bedrock-openai-benchmark/blog/assets/fsi-copilot-thumb.png">
<source src="https://lokahq.github.io/gpt-bedrock-openai-benchmark/blog/assets/fsi-copilot-demo.mp4" type="video/mp4" />
</video>
<figcaption>The FSI Evidence Copilot launch walkthrough: case brief → checklist → cash-flow chart → draft packet, with the decision boundary held throughout.</figcaption>
</figure>

<h2 id="healthcare-the-clinical-evidence-copilot">Healthcare: the Clinical Evidence Copilot</h2>

<p>Swap the industry, keep the pattern. Utilization review and prior authorization are
the same evidence-assembly problem with different policies: find the relevant
policy, locate every qualifying fact in a messy chart, and decide what is missing,
all while keeping the same hard boundary around the judgment. The Clinical Evidence
Copilot does the assembly; a clinician makes the call.</p>

<table>
  <thead>
    <tr>
      <th>Question</th>
      <th>Answer</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>What is manual today?</td>
      <td>A reviewer reads chart notes, labs, medication history, and coverage policy to determine whether the file is ready for review.</td>
    </tr>
    <tr>
      <td>What does the copilot do?</td>
      <td>It maps each policy criterion to chart evidence, labels stale or missing documentation, and exports a draft packet.</td>
    </tr>
    <tr>
      <td>Where does the human stay in control?</td>
      <td>A clinician still decides coverage, medical necessity, or next clinical action. The agent never declares eligibility.</td>
    </tr>
    <tr>
      <td>What changes operationally?</td>
      <td>Faster chart prep, clearer missing-document requests, and a review record that can be defended.</td>
    </tr>
  </tbody>
</table>

<p>It works one synthetic patient at a time, and for every claim it cites a source ID returned by its tools:</p>

<table>
  <thead>
    <tr>
      <th>Tool</th>
      <th>What it does</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">clinical_evidence_workspace</code></td>
      <td>Patient brief, timeline, criteria mapping, and missing-documentation review.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">list/read/save_clinical_policy</code></td>
      <td>Inspect Markdown coverage policies, or draft one (flagged for human review).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">save_clinical_evidence_packet</code></td>
      <td>Export a draft evidence packet as a downloadable artifact.</td>
    </tr>
  </tbody>
</table>

<h3 id="demo-1-home-oxygen-therapy-cec-1003">Demo 1: Home oxygen therapy (CEC-1003)</h3>

<p>A 71-year-old synthetic patient requests a home oxygen concentrator. There is no
policy on file, so the copilot drafts one (marked <code class="language-plaintext highlighter-rouge">DRAFT, REQUIRES HUMAN
REVIEW</code>), then maps 17 criteria to the chart: 11 supported, 4 needing review,
2 missing. It surfaces the clinically decisive fact: a six-minute walk test where
SpO₂ fell to 86% on room air and recovered to 93% on 2 L/min. It also clearly
flags the gaps a reviewer must close, like the absent ICD-10 code.</p>

<h3 id="demo-2-glp-1-diabetes-medication-review-cec-1001">Demo 2: GLP-1 diabetes medication review (CEC-1001)</h3>

<p>A 56-year-old synthetic patient is up for a GLP-1 (semaglutide) review. The copilot
pulls the chart timeline, maps the policy checklist, and lands on the kind of
subtle, easy-to-miss finding that makes or breaks a review:</p>

<p><img src="../assets/images/openai/image_8.png" alt="openai" /></p>
<figure class="post-video">
<video controls="" preload="metadata" poster="https://lokahq.github.io/gpt-bedrock-openai-benchmark/blog/assets/clinical-glp1-poster.png">
<source src="https://lokahq.github.io/gpt-bedrock-openai-benchmark/blog/assets/clinical-glp1-demo.mp4" type="video/mp4" />
</video>
<figcaption>The Clinical Evidence Copilot preparing the GLP-1 review (CEC-1001) end to end.</figcaption>
</figure>

<p>The final chart gives reviewers a readiness view. Green means the file has
evidence for that criterion. Amber means the criterion may be supportable but needs
human review, for example, stale A1c or
incomplete safety screening. Red means the record is missing something the reviewer
should request before spending time on a final determination.</p>

<p><img src="../assets/images/openai/image_9.png" alt="openai" /></p>

<p>Criteria status by review. The useful signal includes what is supported, stale, ambiguous, or missing before the clinician opens the chart.</p>

<h2 id="the-shared-pattern-evidence-over-decisions">The shared pattern: evidence over decisions</h2>

<p>These two copilots feel like the same product because of the operating contract
baked into the product behavior and enforced by guardrails. In regulated work, this
contract is the difference between a tool people are allowed to use and a demo that
never ships:</p>

<ul>
  <li><strong>Cite everything.</strong> Every material claim references a source ID (a chart note, a bank statement, an alert) or a policy section. No uncited assertions.</li>
  <li><strong>Name the gaps as loudly as the matches.</strong> Each criterion gets exactly one status: <em>found</em>, <em>not found</em>, <em>unclear</em>, or <em>needs human review</em>. Stale and missing evidence are first-class outputs.</li>
  <li><strong>Refuse the final call.</strong> No “approved,” “denied,” “eligible,” or “file the SAR.” The copilot drafts; a licensed human decides.</li>
  <li><strong>Stay in scope.</strong> One patient or one case at a time, synthetic data only, no invented facts.</li>
</ul>

<p>Why the boundary helps. The temptation with a capable model is
to let it decide. In healthcare and financial services, that judgment belongs
with a qualified human. By designing the agent to be
excellent at assembly and deliberately incapable of determination,
the human reviewer gets leverage without inheriting a black-box decision they
can’t defend.</p>

<h2 id="from-migration-to-production">From migration to production</h2>

<p>The reason we can present two copilots instead of one is that the architecture is
service-oriented and repeatable without exposing business logic as product glue. A
production agent service needs clear separation between the model path, domain
services, policy assets, authentication, session controls, observability, guardrails,
evaluation, and deployment.</p>

<p>In practical terms, that means teams can migrate or extend OpenAI workloads by
changing the model adapter and deployment posture while preserving the surrounding
service: identity, audit trails, scoped retrieval, token streaming, artifact
generation, monitoring, and human-review controls. Bedrock becomes the governed
model path, while the product experience and domain services remain stable.</p>

<p>Because the model path lives behind Bedrock, choosing OpenAI for the next
engagement becomes a platform decision the team can make per workload:
<code class="language-plaintext highlighter-rouge">openai.gpt-5.5</code> for the hardest reasoning, <code class="language-plaintext highlighter-rouge">openai.gpt-5.4</code>
for price-performance-sensitive professional work, or an open-weight
<code class="language-plaintext highlighter-rouge">openai.gpt-oss-120b</code> / <code class="language-plaintext highlighter-rouge">openai.gpt-oss-20b</code> model when
throughput and cost dominate. The product architecture isolates the adapter from
the review workflow, so adapters can change without disturbing the operating model.</p>

<p>This is the same muscle behind 500+ GenAI and agentic launches since 2023, more
than half of which are moving to or already running in production. The launch of
OpenAI on Bedrock keeps that playbook intact and adds a powerful new option to
the one slot in it that was always meant to be swappable.</p>

<h2 id="conclusion">Conclusion</h2>

<p>OpenAI models on Amazon Bedrock give enterprises a path to build secure, scalable
GenAI inside the AWS ecosystem they already trust. The two copilots here make the
case in practical terms: one architecture pattern, one human-in-the-loop discipline, two
regulated industries, and a model/provider boundary narrow enough to change without
rebuilding the product.</p>

<p>If you’re trying to move from AI experimentation to production at scale, this is the
conversation we want to have. Let’s talk about what an evidence-first copilot looks
like for your team, built with OpenAI on Amazon Bedrock by a launch partner who
has taken hundreds of GenAI and agentic systems from idea to launch.</p>

<p>Catch the announcement →
OpenAI models and Codex on Amazon Bedrock are now generally available.
Want to build with OpenAI on Amazon Bedrock? Let’s connect.</p>

<h2 id="further-reading">Further reading</h2>

<ol>
  <li>AWS (2026). <em>OpenAI models and Codex on Amazon Bedrock are now generally available.</em>
AWS Machine Learning Blog.
<a href="https://aws.amazon.com/blogs/machine-learning/openai-models-and-codex-on-amazon-bedrock-are-now-generally-available/">aws.amazon.com</a></li>
  <li>AWS (2026). <em>APIs supported by Amazon Bedrock.</em>
API support, endpoints, and model invocation reference.
<a href="https://docs.aws.amazon.com/bedrock/latest/userguide/apis.html">docs.aws.amazon.com</a></li>
  <li>AWS (2026). <em>Get started with OpenAI GPT-5.5, GPT-5.4 models, and Codex on Amazon Bedrock.</em>
AWS News Blog.
<a href="https://aws.amazon.com/blogs/aws/get-started-with-openai-gpt-5-5-gpt-5-4-models-and-codex-on-amazon-bedrock/">aws.amazon.com</a></li>
  <li>AWS (2026). <em>Inference using Responses API.</em>
Amazon Bedrock User Guide.
<a href="https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html">docs.aws.amazon.com</a></li>
  <li>AWS (2026). <em>GPT-5.5 model card.</em>
Programmatic access details for the GPT-5.5 Bedrock Mantle endpoint.
<a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-55.html">docs.aws.amazon.com</a></li>
  <li>OpenAI (2026). <em>OpenAI frontier models and Codex are now available on AWS.</em>
<a href="https://openai.com/index/openai-frontier-models-and-codex-are-now-available-on-aws/">openai.com</a></li>
  <li>OpenAI (2026). <em>API pricing.</em>
Model token pricing, Batch pricing, and reasoning-token billing notes.
<a href="https://platform.openai.com/docs/pricing/">platform.openai.com</a></li>
  <li>Datacurve (2026). <em>DeepSWE.</em>
Long-horizon software engineering benchmark and leaderboard.
<a href="https://deepswe.datacurve.ai/">deepswe.datacurve.ai</a></li>
  <li>Artificial Analysis (2026). <em>GPT-5.5: API provider comparison.</em>
Independent cross-provider measurements of output speed, latency, and price,
at <a href="https://artificialanalysis.ai/models/gpt-5-5/providers">xhigh</a>
and <a href="https://artificialanalysis.ai/models/gpt-5-5-medium/providers">medium</a>
reasoning effort.</li>
  <li>Amazon (2026). <em>OpenAI and Amazon announce strategic partnership.</em>
The $50B investment and the ~2 GW Trainium capacity commitment.
<a href="https://www.aboutamazon.com/news/aws/amazon-open-ai-strategic-partnership-investment">aboutamazon.com</a></li>
  <li>Amazon (2026). <em>AWS and Cerebras collaboration aims to set a new standard
for AI inference speed and performance in the cloud.</em>
Disaggregated inference: Trainium prefill, Cerebras wafer-scale decode.
<a href="https://press.aboutamazon.com/aws/2026/3/aws-and-cerebras-collaboration-aims-to-set-a-new-standard-for-ai-inference-speed-and-performance-in-the-cloud">press.aboutamazon.com</a></li>
  <li>The New Stack (2026). <em>AWS lands OpenAI on Bedrock, but Trainium is the
real story.</em> Analysis of the silicon strategy behind the Bedrock launch.
<a href="https://thenewstack.io/openai-bedrock-trainium-silicon/">thenewstack.io</a></li>
  <li>OpenAI (2026). <em>Codex for every role, tool, and workflow.</em>
Codex usage and adoption update.
<a href="https://openai.com/index/codex-for-every-role-tool-workflow/">openai.com</a></li>
</ol>]]></content><author><name>shekswess</name></author><category term="AI" /><category term="LLM" /><category term="OpenAI" /><category term="Amazon Bedrock" /><category term="GPT-5.5" /><category term="Codex" /><category term="AWS" /><category term="Benchmarking" /><category term="Agentic AI" /><summary type="html"><![CDATA[Benchmarking GPT-5.5 on Amazon Bedrock, then building two regulated-industry copilots on the path that passed.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://lokahq.github.io/gpt-bedrock-openai-benchmark/blog/assets/cover-banner-gpt.png" /><media:content medium="image" url="https://lokahq.github.io/gpt-bedrock-openai-benchmark/blog/assets/cover-banner-gpt.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Running Hugging Face’s Carbon on AWS Trainium2: The Engineering Walkthrough</title><link href="https://shekswess.github.io/running-hugging-faces-carbon-on-aws-trainium2-with-nxd-inference.html" rel="alternate" type="text/html" title="Running Hugging Face’s Carbon on AWS Trainium2: The Engineering Walkthrough" /><published>2026-05-20T16:00:00+00:00</published><updated>2026-05-20T16:00:00+00:00</updated><id>https://shekswess.github.io/running-hugging-faces-carbon-on-aws-trainium2-with-nxd-inference</id><content type="html" xml:base="https://shekswess.github.io/running-hugging-faces-carbon-on-aws-trainium2-with-nxd-inference.html"><![CDATA[<p><em><strong>Original Source of the blog post: <a href="https://medium.com/loka-engineering/running-hugging-faces-carbon-on-aws-trainium2-with-nxd-inference-ecdee6c2f4ce">Running Hugging Face’s Carbon on AWS Trainium2 with NxD Inference</a></strong></em></p>

<p><em><strong>Written by Bojan Jakimovski</strong></em></p>

<p>The Engineering Walkthrough</p>

<p><a href="https://huggingface.co/collections/HuggingFaceBio/carbon">Hugging Face Bio’s Carbon</a> release is interesting for two reasons at once. First, it is a biology model. Second, it is not an infrastructure outlier. Many genomic models come with custom architectures, specialized operators, or runtime assumptions that make deployment harder to evaluate. Carbon is different: it is an open DNA foundation model family with 500M, 3B, and 8B checkpoints, Apache 2.0 weights, a Llama-style causal backbone, and a DNA-native 6-mer tokenizer.</p>

<p>That made it a natural Trainium2 test, with one concrete question behind it:</p>

<blockquote>
  <p>Can a freshly released open genomic model run through AWS NxD Inference on a single <code class="language-plaintext highlighter-rouge">trn2.3xlarge</code> without rewriting the model?</p>
</blockquote>

<p>The short answer is yes. We compiled and benchmarked all three Carbon checkpoints on one <code class="language-plaintext highlighter-rouge">trn2.3xlarge</code>: one Trainium2 accelerator, four logical NeuronCores, 96 GB of accelerator memory, BF16 weights, and tensor parallel degree 4.</p>

<p>This post is the <a href="https://github.com/LokaHQ/carbon-neuronx-distributed-inference">code-side companion</a> to the main benchmark writeup. The main article covers the business and platform story. This one covers the practical path: tokenizer handling, compile shape, benchmark harness, result interpretation, and what did not improve the baseline.</p>

<p>For the full report, including the Trainium2 business case, A100 reference comparison, and cost-normalized throughput charts, read the main Loka writeup: <a href="https://lokahq.github.io/carbon-neuronx-distributed-inference/">Running Hugging Face Carbon on AWS Trainium2 with NxD Inference</a>.</p>

<h3 id="why-carbon-was-a-good-fit-for-nxd-inference">Why Carbon Was a Good Fit for NxD Inference</h3>

<p>Carbon is domain-specific, but its architecture is deliberately familiar. Under the hood, the configuration advertises a Llama causal model path. That matters because NxD Inference already has strong support for Llama-style autoregressive serving on Trainium and Inferentia.</p>

<p>The unusual part is the tokenizer. Carbon uses non-overlapping 6-mer DNA tokens. Instead of treating every nucleotide as one token, it groups six bases into one model token. A 1080 bp DNA sequence becomes roughly 180 Carbon DNA tokens when the tokenizer is in DNA mode.</p>

<p>That token efficiency is the reason the benchmark is operationally interesting. Shorter token sequences reduce attention cost, keep the runtime path close to a standard Transformer serving stack, and make a single Trainium2 accelerator a credible first deployment unit.</p>

<h3 id="tested-platform">Tested Platform</h3>

<p>The measured runs used:</p>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Setting</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Instance</td>
      <td><code class="language-plaintext highlighter-rouge">trn2.3xlarge</code></td>
    </tr>
    <tr>
      <td>Accelerator</td>
      <td>1 Trainium2 device, 4 logical NeuronCores, <code class="language-plaintext highlighter-rouge">LNC=2</code></td>
    </tr>
    <tr>
      <td>Runtime</td>
      <td>PyTorch 2.9.1, torch-neuronx 2.9.0, torch-xla 2.9.0, Neuron SDK 2.29 stack</td>
    </tr>
    <tr>
      <td>NxDI</td>
      <td><code class="language-plaintext highlighter-rouge">neuronx-distributed-inference==0.9.0</code></td>
    </tr>
    <tr>
      <td>Parallelism</td>
      <td><code class="language-plaintext highlighter-rouge">tp_degree=4</code>, <code class="language-plaintext highlighter-rouge">batch_size=16</code> for the A100-style benchmark</td>
    </tr>
    <tr>
      <td>Baseline shape</td>
      <td><code class="language-plaintext highlighter-rouge">max_context_length=256</code>, <code class="language-plaintext highlighter-rouge">seq_len=512</code>, <code class="language-plaintext highlighter-rouge">max_new_tokens=170</code></td>
    </tr>
    <tr>
      <td>Prompt format</td>
      <td>DNA sequence wrapped as <code class="language-plaintext highlighter-rouge">&lt;dna&gt;...&lt;/dna&gt;</code></td>
    </tr>
  </tbody>
</table>

<p>The most important non-obvious setting is <code class="language-plaintext highlighter-rouge">LNC=2</code>, which exposes four logical NeuronCores from the single Trainium2 device. We used <code class="language-plaintext highlighter-rouge">tp_degree=4</code> so the model shards across those logical cores.</p>

<h3 id="bootstrap-the-environment">Bootstrap the Environment</h3>

<p>Start from a Neuron DLAMI or Neuron DLC where the Neuron driver and runtime are already configured. The repository helper script does three things: creates the Python environment, installs the Neuron SDK 2.29/PyTorch 2.9 stack, and installs a patched NxD Inference checkout.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone &lt;repository-url&gt;
<span class="nb">cd </span>carbon-neuronx-distributed-inference
<span class="nv">NXDI_REF</span><span class="o">=</span>main ./scripts/bootstrap_nxdi.sh
<span class="nb">source</span> .venv-carbon-sdk29/bin/activate
</code></pre></div></div>

<p>The Carbon-specific NxD Inference patch is intentionally small. The important point is that Carbon does not require a model rewrite or model-specific kernels for the baseline path. The compatibility work is around configuration/tokenizer handling and keeping the model on NxDI’s existing Llama causal path.</p>

<h3 id="download-the-carbon-checkpoints">Download the Carbon Checkpoints</h3>

<p>Carbon uses Hugging Face custom tokenizer code, so make sure the checkpoints are downloaded with the repository files intact.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>huggingface-cli login
./scripts/download_carbon_models.sh <span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/models/carbon"</span>
</code></pre></div></div>

<p>To download a single checkpoint:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">MODELS</span><span class="o">=</span><span class="s2">"Carbon-500M"</span> ./scripts/download_carbon_models.sh <span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/models/carbon"</span>
</code></pre></div></div>

<h3 id="the-tokenizer-detail-that-matters">The Tokenizer Detail That Matters</h3>

<p>This is the part most likely to poison the benchmark if missed. Carbon requires trusted custom tokenizer code. DNA prompts should also use Carbon’s DNA mode prefix: <code class="language-plaintext highlighter-rouge">&lt;dna&gt;{sequence}</code>. Without those two details, the model may still run, but you are not measuring the intended compact 6-mer DNA path.</p>

<p>In Python:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">transformers</span> <span class="kn">import</span> <span class="n">AutoTokenizer</span>

<span class="n">tokenizer</span> <span class="o">=</span> <span class="n">AutoTokenizer</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span>
    <span class="s">"/home/ubuntu/models/carbon/Carbon-500M"</span><span class="p">,</span>
    <span class="n">trust_remote_code</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
    <span class="n">padding_side</span><span class="o">=</span><span class="s">"right"</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">tokenizer</span><span class="p">.</span><span class="n">pad_token_id</span> <span class="o">=</span> <span class="mi">151643</span>
<span class="n">inputs</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="p">(</span>
    <span class="p">[</span><span class="s">"&lt;dna&gt;ACGTTGCAACGTTGCAACGTTGCA"</span><span class="p">],</span>
    <span class="n">return_tensors</span><span class="o">=</span><span class="s">"pt"</span><span class="p">,</span>
    <span class="n">padding</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<p>From the NxD Inference demo entrypoint, the same ideas show up as CLI flags and prompt formatting:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>inference_demo <span class="se">\</span>
 <span class="nt">-model-type</span> llama <span class="se">\</span>
 <span class="nt">-task-type</span> causal-lm <span class="se">\</span>
 run <span class="se">\</span>
 <span class="nt">-model-path</span> <span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/models/carbon/Carbon-500M"</span> <span class="se">\</span>
 <span class="nt">-compiled-model-path</span> <span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/compiled/carbon/Carbon-500M-tp4-bs16-s512-c256-n170-bf16"</span> <span class="se">\</span>
 <span class="nt">-trust-remote-code</span> <span class="se">\</span>
 <span class="nt">-pad-token-id</span> 151643 <span class="se">\</span>
 <span class="nt">-prompt</span> <span class="s2">"&lt;dna&gt;ACGTTGCAACGTTGCAACGTTGCAACGTTGCA"</span>
</code></pre></div></div>

<p>The pad token id used in the measured artifacts was <code class="language-plaintext highlighter-rouge">151643</code>.</p>

<h3 id="compile-the-baseline-artifacts">Compile the Baseline Artifacts</h3>

<p>The A100-style benchmark shape mirrors the high-level workload used in the Carbon technical report: 16 DNA prompts, approximately 1020 bp input per prompt, and approximately 1020 bp generated per prompt.</p>

<p>The compile defaults were:</p>

<table>
  <thead>
    <tr>
      <th>Setting</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Tensor parallel degree</td>
      <td><code class="language-plaintext highlighter-rouge">TP_DEGREE=4</code></td>
    </tr>
    <tr>
      <td>Batch size</td>
      <td><code class="language-plaintext highlighter-rouge">BATCH_SIZE=16</code></td>
    </tr>
    <tr>
      <td>Max context length</td>
      <td><code class="language-plaintext highlighter-rouge">MAX_CONTEXT_LENGTH=256</code></td>
    </tr>
    <tr>
      <td>Sequence length</td>
      <td><code class="language-plaintext highlighter-rouge">SEQ_LEN=512</code></td>
    </tr>
    <tr>
      <td>Max length</td>
      <td><code class="language-plaintext highlighter-rouge">MAX_LENGTH=512</code></td>
    </tr>
    <tr>
      <td>Max new tokens</td>
      <td><code class="language-plaintext highlighter-rouge">MAX_NEW_TOKENS=170</code></td>
    </tr>
    <tr>
      <td>Pad token id</td>
      <td><code class="language-plaintext highlighter-rouge">PAD_TOKEN_ID=151643</code></td>
    </tr>
    <tr>
      <td>Logical NeuronCore config</td>
      <td><code class="language-plaintext highlighter-rouge">LOGICAL_NC_CONFIG=2</code></td>
    </tr>
    <tr>
      <td>Dtype</td>
      <td><code class="language-plaintext highlighter-rouge">bfloat16</code></td>
    </tr>
    <tr>
      <td>Sampling</td>
      <td>on-device sampling, <code class="language-plaintext highlighter-rouge">top_k=1</code>, <code class="language-plaintext highlighter-rouge">top_p=1.0</code>, <code class="language-plaintext highlighter-rouge">temperature=1.0</code></td>
    </tr>
  </tbody>
</table>

<p>Compile all three baseline artifacts:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/compile_carbon.sh Carbon-500M
./scripts/compile_carbon.sh Carbon-3B
./scripts/compile_carbon.sh Carbon-8B
</code></pre></div></div>

<p>Compiled artifacts are written under <code class="language-plaintext highlighter-rouge">$HOME/compiled/carbon/</code>.</p>

<h3 id="run-the-a100-style-batch-benchmark">Run the A100-Style Batch Benchmark</h3>

<p>The batch benchmark creates 16 synthetic DNA prompts and measures generated base-pair throughput. Generation is greedy direct 6-mer decoding, with Factorized Nucleotide Supervision base-pair marginal decoding disabled.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/run_a100_style_benchmark.sh Carbon-500M
./scripts/run_a100_style_benchmark.sh Carbon-3B
./scripts/run_a100_style_benchmark.sh Carbon-8B
</code></pre></div></div>

<p>The benchmark harness does the same basic flow you would expect from a Hugging Face generation adapter:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tokenizer</span> <span class="o">=</span> <span class="n">AutoTokenizer</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span>
    <span class="n">args</span><span class="p">.</span><span class="n">model_path</span><span class="p">,</span>
    <span class="n">trust_remote_code</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
    <span class="n">padding_side</span><span class="o">=</span><span class="s">"right"</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">tokenizer</span><span class="p">.</span><span class="n">pad_token_id</span> <span class="o">=</span> <span class="n">args</span><span class="p">.</span><span class="n">pad_token_id</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">NeuronLlamaForCausalLM</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">compiled_model_path</span><span class="p">)</span>
<span class="n">model</span><span class="p">.</span><span class="n">load</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">compiled_model_path</span><span class="p">)</span>
<span class="n">generation_model</span> <span class="o">=</span> <span class="n">HuggingFaceGenerationAdapter</span><span class="p">(</span><span class="n">model</span><span class="p">)</span>
<span class="n">prompts</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s">"&lt;dna&gt;</span><span class="si">{</span><span class="n">case</span><span class="p">.</span><span class="n">sequence</span><span class="si">}</span><span class="s">"</span> <span class="k">for</span> <span class="n">case</span> <span class="ow">in</span> <span class="n">cases</span><span class="p">]</span>
<span class="n">inputs</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="p">(</span><span class="n">prompts</span><span class="p">,</span> <span class="n">return_tensors</span><span class="o">=</span><span class="s">"pt"</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<p>For each timed run, the harness resets the model, generates the batch, decodes the generated suffix, gets A/C/G/T characters, and reports tokens/sec, bp/sec, and kbp/sec.</p>

<h3 id="baseline-results">Baseline Results</h3>

<p>On one <code class="language-plaintext highlighter-rouge">trn2.3xlarge</code>, the batch-16 A100-style run measured:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>p50 elapsed</th>
      <th>Token throughput</th>
      <th>Base-pair throughput</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Carbon-500M</td>
      <td>0.867 s</td>
      <td>3137.6 tok/s</td>
      <td>18.83 kbp/s</td>
    </tr>
    <tr>
      <td>Carbon-3B</td>
      <td>1.484 s</td>
      <td>1833.0 tok/s</td>
      <td>11.00 kbp/s</td>
    </tr>
    <tr>
      <td>Carbon-8B</td>
      <td>1.986 s</td>
      <td>1369.6 tok/s</td>
      <td>8.21 kbp/s</td>
    </tr>
  </tbody>
</table>

<p>The main read is not that this is a perfect GPU-vs-Trainium shootout. The A100 reference in the Carbon report uses vLLM dynamic batching on a single A100-80GB. This Trainium2 run uses a static compiled NxDI batch on one <code class="language-plaintext highlighter-rouge">trn2.3xlarge</code>. The useful read is operational: Carbon can run quickly on a small AWS-native Trainium2 unit immediately, with a path to larger Trn2 fleet shapes once demand justifies it.</p>

<h3 id="run-the-batch-1-dna-prompt-suite">Run the Batch-1 DNA Prompt Suite</h3>

<p>The public article also reports a batch-1 prompt suite. That suite is not the main cost comparison; it is a sanity check over different synthetic DNA regimes.</p>

<p>The prompt cases were:</p>

<ul>
  <li>Balanced DNA</li>
  <li>CpG-rich</li>
  <li>AT-rich</li>
  <li>GC-rich</li>
  <li>ORF-like</li>
  <li>TATA-box-like</li>
  <li>CAG triplet-repeat</li>
</ul>

<p>Compile a batch-1 artifact first:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">BATCH_SIZE</span><span class="o">=</span>1 <span class="nv">MAX_NEW_TOKENS</span><span class="o">=</span>180 ./scripts/compile_carbon.sh Carbon-500M
</code></pre></div></div>

<p>Then run the suite:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/run_generation_suite.sh Carbon-500M
./scripts/run_generation_suite.sh Carbon-3B
./scripts/run_generation_suite.sh Carbon-8B
</code></pre></div></div>

<p>The batch-1 median results were:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Median elapsed</th>
      <th>Median token throughput</th>
      <th>Median bp throughput</th>
      <th>Avg valid DNA</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Carbon-500M</td>
      <td>0.689 s</td>
      <td>261.3 tok/s</td>
      <td>1568 bp/s</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Carbon-3B</td>
      <td>1.277 s</td>
      <td>140.9 tok/s</td>
      <td>846 bp/s</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Carbon-8B</td>
      <td>1.589 s</td>
      <td>113.3 tok/s</td>
      <td>679 bp/s</td>
      <td>99.0%</td>
    </tr>
  </tbody>
</table>

<p>Carbon-8B’s 99% valid-DNA average came from the CAG triplet-repeat stress case, where part of the generated stream moved outside pure DNA vocabulary. That is not a Trainium issue, but it is a useful measurement caveat: base-pair throughput is cleanest when the decoded stream remains in DNA mode.</p>

<h3 id="what-we-would-optimize-in-near-future">What We Would Optimize in Near Future</h3>

<p>Carbon-3B is the cleanest tensor-parallel fit under <code class="language-plaintext highlighter-rouge">tp_degree=4</code> because it has four KV heads. Carbon-500M and Carbon-8B compile and run under <code class="language-plaintext highlighter-rouge">TP=4</code>, but they trigger a GQA-to-MHA sharding fallback. That makes GQA layout and kernel eligibility the first place we would look.</p>

<p>The next serious pass should profile:</p>

<ul>
  <li>context encoding versus token generation time</li>
  <li>attention kernel eligibility</li>
  <li>QKV and output projection paths</li>
  <li>RoPE and normalization paths</li>
  <li>bucket shapes for real serving traffic, not only benchmark symmetry</li>
  <li>batch shape tradeoffs for scanning, scoring, and generation workloads</li>
</ul>

<p>The key moment is not to add custom kernels because they sound impressive. Add them only after profiling proves that a hot path is blocking the workload.</p>

<h3 id="caveats">Caveats</h3>

<p>This benchmark is intentionally narrow:</p>

<ul>
  <li>It measures direct 6-mer generation, not full FNS (Factorized Nucleotide Supervision) base-pair marginal decoding or scoring.</li>
  <li>It is a throughput and compatibility proof, not a biological quality evaluation.</li>
  <li>The A100 comparison is a reference comparison, not a controlled accelerator shootout.</li>
  <li>The measured economics use public Capacity Blocks prices and do not include orchestration, storage, utilization loss, or production serving overhead.</li>
</ul>

<p>Those caveats matter, but they do not weaken the main engineering result: Carbon can move from a fresh Hugging Face release to a compiled Trainium2 serving artifact quickly.</p>

<h3 id="takeaway">Takeaway</h3>

<p>The important part is not only the throughput number. It is the deployment shape.</p>

<p>Carbon gives teams an open DNA model family. Trainium2 gives them a small AWS-native accelerator unit with 96 GB of memory. NxD Inference gives them a standard compiled serving path. Together, that turns “interesting biology model” into something much closer to an operational artifact.</p>

<p>For bio and HCLS teams, that is the difference between a model demo and a procurement-ready infrastructure conversation.</p>]]></content><author><name>shekswess</name></author><category term="AI" /><category term="LLM" /><category term="AWS Trainium2" /><category term="NxD Inference" /><category term="Neuron" /><category term="Hugging Face" /><category term="Carbon" /><category term="Bio AI" /><category term="Genomics" /><summary type="html"><![CDATA[The engineering walkthrough for running Hugging Face Bio Carbon on a single AWS Trainium2 instance with NxD Inference.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://lokahq.github.io/carbon-neuronx-distributed-inference/assets/carbon-trainium2-cover.png" /><media:content medium="image" url="https://lokahq.github.io/carbon-neuronx-distributed-inference/assets/carbon-trainium2-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">A First NxD Inference Benchmark for Open DNA Foundation Models</title><link href="https://shekswess.github.io/running-hugging-face-carbon-on-aws-trainium2-with-nxd-inference.html" rel="alternate" type="text/html" title="A First NxD Inference Benchmark for Open DNA Foundation Models" /><published>2026-05-19T16:00:00+00:00</published><updated>2026-05-19T16:00:00+00:00</updated><id>https://shekswess.github.io/running-hugging-face-carbon-on-aws-trainium2-with-nxd-inference</id><content type="html" xml:base="https://shekswess.github.io/running-hugging-face-carbon-on-aws-trainium2-with-nxd-inference.html"><![CDATA[<p><em><strong>Original Source of the blog post: <a href="https://lokahq.github.io/carbon-neuronx-distributed-inference/">Running Hugging Face Carbon on AWS Trainium2 with NxD Inference</a></strong></em></p>

<p><em><strong>Written by Bojan Jakimovski</strong></em></p>

<h2 id="introduction">Introduction</h2>

<p>The Human Genome Project, completed in 2003, cost around <a href="https://www.genome.gov/about-genomics/fact-sheets/Sequencing-Human-Genome-cost">$3 billion</a> over 13 years to sequence the first human genome. Today, the sequencing bill is measured in hundreds of dollars. That changes the problem. For many bio and HCLS teams, the hard part is no longer getting enough sequence data. The hard part is turning that data into something useful: variant effect prediction, regulatory annotation, gene expression modeling, therapeutic sequence design, and repeated scoring runs that need to happen inside a secure cloud environment.</p>

<p>This is where DNA foundation models become interesting. The idea is simple enough: DNA is a sequence, and modern foundation models are extremely good at learning from sequences. Models such as <a href="https://arxiv.org/abs/2306.15794">HyenaDNA</a>, the <a href="https://www.nature.com/articles/s41592-024-02523-z">Nucleotide Transformer</a>, and <a href="https://arcinstitute.org/tools/evo">Evo2</a> showed that models trained on raw genomic sequence can learn useful biological representations without hand-built feature pipelines. But many of these releases also came with a practical catch: unusual architectures, closed or constrained access, large GPU requirements, or tokenization choices that make long-context inference expensive.</p>

<p><a href="https://huggingface.co/spaces/HuggingFaceBio/carbon-demo">Carbon</a>, released by Hugging Face Bio, is a very good step in the other direction. It is open, Apache 2.0, available in 500M, 3B, and 8B sizes, and trained on roughly six trillion base pairs of eukaryotic genomic DNA, mRNA transcripts, and bacterial genomes. It uses a 6-mer tokenizer, so six DNA bases become one model token, while Factorized Nucleotide Supervision keeps the training objective aware of base-level prediction. Carbon-3B is reported to match <a href="https://arcinstitute.org/tools/evo">Evo2-7B</a> on key DNA benchmarks while being smaller and much faster. For infrastructure teams, the biggest gift is even more practical: Carbon is built on a familiar <a href="https://arxiv.org/abs/2407.21783">Llama 3</a>-style Transformer backbone.</p>

<p>That matters because a model card is not a production deployment. Bio teams usually want the model running inside an AWS VPC, close to their data, with security and procurement teams comfortable with the path. They also need hardware they can actually reserve. The A100-80GB reference used in the Carbon report is a strong baseline, but on AWS that capacity is commonly exposed through larger nodes such as <code class="language-plaintext highlighter-rouge">p4de.24xlarge</code>. For a team that wants to test one genomics model, that can mean reserving an 8-GPU node when the first workload does not need eight GPUs.</p>

<p>So when Carbon shipped, we tried the thing customers actually ask us about at Loka: can we take this fresh Hugging Face checkpoint and make it run on AWS-native accelerator infrastructure without turning the first experiment into a custom kernel project?</p>

<p>The answer was yes. On launch day, we ran Carbon-500M, Carbon-3B, and Carbon-8B on a single <code class="language-plaintext highlighter-rouge">trn2.3xlarge</code> using <a href="https://github.com/aws-neuron/neuronx-distributed-inference">NxD Inference</a>: one Trainium2 accelerator, 96 GB of accelerator memory, and a public Capacity Blocks rate of about <a href="https://aws.amazon.com/ec2/capacityblocks/pricing/">$2.23/hr</a>, checked on May 21, 2026. No model rewrite. No model-specific kernels. A small tokenizer/config compatibility patch was enough for Carbon to ride the existing NxDI Llama causal path.</p>

<p>In the batch-16 sweep shaped after the <a href="https://github.com/huggingface/carbon/blob/main/tech-report.pdf">Carbon technical report</a>’s A100 reference, Carbon-500M reached <strong>18.83 kbp/s</strong>, Carbon-3B reached <strong>11.00 kbp/s</strong>, and Carbon-8B reached <strong>8.21 kbp/s</strong>. This is not a final service benchmark and it is not a biological evaluation. It is something more basic, and honestly more useful for executives: proof that the distance between an open model release and a production-shaped AWS serving artifact is getting shorter.</p>

<h2 id="what-makes-carbon-different">What makes Carbon different</h2>

<p><strong>Why this section matters:</strong> Carbon is not “just another LLM for DNA.” It is open, it uses a familiar Llama-style backbone, and it adds the DNA-specific pieces where they actually matter: tokenization and training. That combination is what made a fast Trainium2 path possible.</p>

<p>The architecture is intentionally familiar: decoder-only autoregressive modeling, RMSNorm, SwiGLU, RoPE, GQA, and a Llama-style causal path. That is not the most exotic design choice, and that is the point. In enterprise AI, boring architecture is often a feature. It means the model has a better chance of working with the compilers, runtimes, profiling tools, and deployment patterns that customers already trust.</p>

<p>The DNA-specific part is the tokenizer. Carbon uses non-overlapping 6-mer tokenization, so six nucleotides become one model token. A 1080 base-pair input becomes roughly 180 DNA tokens in DNA mode. That compression is what makes long genomic spans more practical on a standard Transformer backbone. Instead of paying attention cost on every single base, the model can work over a shorter token sequence while still learning from nucleotide-level supervision during training.</p>

<p>The tradeoff is also worth being honest about. Single-base tokenization gives a cleaner interface, but it makes every sequence six times longer. Carbon takes the more deployment-friendly route: compact 6-mer tokens, plus Factorized Nucleotide Supervision to keep the model aware of base-level prediction. For this Trainium2 benchmark, we measured direct 6-mer generation. FNS base-pair-level inference is the next obvious follow-up because it would make the biological interface even cleaner.</p>

<p>This is the part we like the most about the release from Hugging Face Bio: Carbon is not only a benchmark result. It is a model family that infrastructure teams can actually touch. The weights are open, the model cards are public, the sizes are practical, and the architecture gives cloud teams a real shot at making it production-shaped without starting from a blank page.</p>

<p>The table below summarizes the three Carbon sizes we tested. The main deployment-relevant difference is not only parameter count, but also the attention shape: hidden size, layer count, attention heads, and KV heads all affect how cleanly the model maps onto tensor parallel inference.</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Hidden</th>
      <th>Layers</th>
      <th>Attention heads</th>
      <th>KV heads</th>
      <th>Vocabulary</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Carbon-500M</td>
      <td>1024</td>
      <td>28</td>
      <td>16</td>
      <td>8</td>
      <td>155,776</td>
    </tr>
    <tr>
      <td>Carbon-3B</td>
      <td>3072</td>
      <td>30</td>
      <td>32</td>
      <td>4</td>
      <td>155,776</td>
    </tr>
    <tr>
      <td>Carbon-8B</td>
      <td>4096</td>
      <td>32</td>
      <td>32</td>
      <td>8</td>
      <td>155,776</td>
    </tr>
  </tbody>
</table>

<h2 id="why-trainium2">Why Trainium2</h2>

<p>Bio workloads are not chat workloads with a lab label on top. They often involve private sequence data, long context, repeated scoring, likelihood comparisons, perturbation sweeps, and compliance constraints. Cost matters, but so do data locality, procurement shape, security review, and the ability to keep the model in the same AWS environment where the rest of the platform already runs.</p>

<p>This is where <a href="https://aws.amazon.com/ai/machine-learning/trainium/">AWS Trainium2</a> deserves real praise. It gives teams an AWS-native accelerator option that is purpose-built for deep learning, starts at a practical one-chip instance size, and scales to larger Trn2 fleets when the workload is proven. For customers already building on AWS, that is not a small detail. It means the accelerator strategy can fit the cloud strategy instead of forcing a separate GPU-only operating model from day one.</p>

<p><strong>Why Trainium2 matters here:</strong> the value is not only lower cost. The value is having an AWS-native accelerator path that starts small, stays inside the customer environment, and can scale when the workload is real.</p>

<p>Carbon was a good test case because it is not a generic chat model, but it is shaped like one. Its configs advertise <code class="language-plaintext highlighter-rouge">LlamaForCausalLM</code>. NxD Inference already has a strong Llama causal path. The bet was simple: if the tokenizer and model config cooperate, Carbon should be able to use the existing NxDI path.</p>

<p>At Loka, this is the question we hear whenever a serious open model ships: not only “is the model good?”, but “how fast can we stand it up on infrastructure we already operate, at a cost we can explain to procurement?” Carbon gave us a clean way to answer that question on real Trainium2 hardware.</p>

<p>The timing also fits the broader AWS BioFM story. In April 2026, AWS published a <a href="https://aws.amazon.com/blogs/machine-learning/applying-multimodal-biological-foundation-models-across-therapeutics-and-patient-care/">multimodal biological foundation-model overview</a> that describes BioFM adoption as a combination of model development, governed biological data, scalable infrastructure, and implementation support. Importantly for us, AWS also names Loka alongside Deloitte and Accenture as implementation partners for organizations moving multimodal BioFM work from proof-of-concept to production. Carbon on Trainium2 is a very concrete example of that motion: an open Hugging Face Bio model, running on AWS accelerator infrastructure, with cost, throughput, and deployment details that enterprise teams can actually evaluate.</p>

<h2 id="benchmark-setup">Benchmark setup</h2>

<p>We ran the benchmark on a single <code class="language-plaintext highlighter-rouge">trn2.3xlarge</code>. The instance exposes one Trainium2 device, four logical NeuronCores, and 96 GB of accelerator memory. The run used tensor parallel degree 4, BF16 weights, batch size 1, on-device sampling, and NxDI bucketing.</p>

<p>The main benchmark shape mirrors the Carbon report’s Figure 1 framing: 1080 base pairs for prefill and 1080 base pairs for decode. With 6-mer DNA tokenization, that is about 180 input tokens and 180 generated tokens. We compiled a 512-token artifact with a 256-token context bucket and a 180-token decode budget. The setup table below captures the exact runtime, parallelism, and prompt shape used for the fixed-shape run.</p>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Setting</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Instance</td>
      <td><code class="language-plaintext highlighter-rouge">trn2.3xlarge</code></td>
    </tr>
    <tr>
      <td>Accelerator</td>
      <td>1 Trainium2 device, 4 logical NeuronCores, <code class="language-plaintext highlighter-rouge">LNC=2</code></td>
    </tr>
    <tr>
      <td>Runtime</td>
      <td>PyTorch 2.9.1, torch-neuronx 2.9.0, torch-xla 2.9.0, Neuron SDK 2.29 stack</td>
    </tr>
    <tr>
      <td>NxDI</td>
      <td><code class="language-plaintext highlighter-rouge">neuronx-distributed-inference==0.9.0</code></td>
    </tr>
    <tr>
      <td>Parallelism</td>
      <td><code class="language-plaintext highlighter-rouge">tp_degree=4</code>, <code class="language-plaintext highlighter-rouge">batch_size=1</code></td>
    </tr>
    <tr>
      <td>Shape</td>
      <td><code class="language-plaintext highlighter-rouge">max_context_length=256</code>, <code class="language-plaintext highlighter-rouge">seq_len=512</code>, <code class="language-plaintext highlighter-rouge">max_new_tokens=180</code></td>
    </tr>
    <tr>
      <td>Prompt</td>
      <td>1080 bp sequence wrapped as <code class="language-plaintext highlighter-rouge">&lt;dna&gt;...&lt;/dna&gt;</code> for the fixed-shape run</td>
    </tr>
  </tbody>
</table>

<h2 id="results-and-analysis">Results and analysis</h2>

<p>The headline result is compatibility. All three Carbon checkpoints compiled and ran through NxDI with tokenizer handling plus a small compatibility/config patch. That is exactly what you want from a standard architecture: a domain model from Hugging Face Bio, running through a general AWS inference stack, without waiting for a bespoke serving path.</p>

<h3 id="fixed-shape-nxdi-throughput">Fixed-shape NxDI throughput</h3>

<p>We first measured compiled NxDI artifacts with the fixed report-shaped configuration. These numbers separate context encoding, token generation, and end-to-end behavior under one static shape, so they are useful for infrastructure comparison and future optimization work. The fixed-shape results table below reports compile/load time, context encode, token generation, and end-to-end latency for each model size.</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Compile + trace</th>
      <th>Load</th>
      <th>CTE p50</th>
      <th>TKG p50</th>
      <th>TKG throughput</th>
      <th>E2E p50</th>
      <th>E2E throughput</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Carbon-500M</td>
      <td>69.73 s</td>
      <td>7.74 s</td>
      <td>5.49 ms</td>
      <td>3.35 ms</td>
      <td>297.0 tok/s</td>
      <td>693.76 ms</td>
      <td>737.5 tok/s</td>
    </tr>
    <tr>
      <td>Carbon-3B</td>
      <td>96.54 s</td>
      <td>9.27 s</td>
      <td>13.14 ms</td>
      <td>6.68 ms</td>
      <td>150.2 tok/s</td>
      <td>1299.10 ms</td>
      <td>394.4 tok/s</td>
    </tr>
    <tr>
      <td>Carbon-8B</td>
      <td>144.33 s</td>
      <td>10.25 s</td>
      <td>18.90 ms</td>
      <td>8.32 ms</td>
      <td>120.4 tok/s</td>
      <td>1604.55 ms</td>
      <td>319.2 tok/s</td>
    </tr>
  </tbody>
</table>

<p><em>Figure 1. Fixed-shape NxDI throughput on <code class="language-plaintext highlighter-rouge">trn2.3xlarge</code>. Token generation throughput is the most relevant number for autoregressive decode; end-to-end throughput reflects the compiled benchmark harness shape.</em></p>

<p>The scaling pattern is healthy. Carbon-500M is fast, Carbon-3B is comfortable on a single small Trainium2 instance, and Carbon-8B fits and runs with useful latency before any model-specific optimization work. For a first-pass enterprise test, that is the important part: the serving path works and the numbers are usable.</p>

<h3 id="dna-prompt-suite">DNA prompt suite</h3>

<p>A fixed-shape benchmark is useful, but it does not tell the whole story. We also ran a seven-prompt DNA suite using 1080 bp prompts: balanced DNA, CpG-rich, AT-rich, GC-rich, ORF-like, TATA-box-like, and CAG triplet-repeat. Each model ran one warmup and two measured runs per case, for 14 measured generations per model. The DNA-suite table below reports the median latency and throughput, plus a simple valid-DNA check on decoded A/C/G/T output.</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Median elapsed</th>
      <th>Median token throughput</th>
      <th>Median bp throughput</th>
      <th>Avg valid DNA</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Carbon-500M</td>
      <td>0.689 s</td>
      <td>261.3 tok/s</td>
      <td>1568 bp/s</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Carbon-3B</td>
      <td>1.277 s</td>
      <td>140.9 tok/s</td>
      <td>846 bp/s</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Carbon-8B</td>
      <td>1.589 s</td>
      <td>113.3 tok/s</td>
      <td>679 bp/s</td>
      <td>99.0%</td>
    </tr>
  </tbody>
</table>

<p><em>Figure 2. Median base-pair throughput for 1080 bp decode across seven DNA prompt regimes. Base-pair throughput is computed from decoded A/C/G/T characters, not assumed from token count.</em></p>

<p>Carbon-500M and Carbon-3B generated 1080 valid base pairs in every prompt case. Carbon-8B did the same for six out of seven cases. On the CAG triplet-repeat stress case, 180 generated tokens decoded to 963 A/C/G/T characters because part of the output moved outside pure DNA vocabulary. That is not a Trainium issue, but it is a useful measurement caveat: base-pair throughput is cleanest when generation stays in DNA mode.</p>

<h3 id="comparison-with-the-a100-baseline">Comparison with the A100 baseline</h3>

<p>Hugging Face Bio reports an A100 reference point in the Carbon technical report. The appendix measures Carbon-3B and Carbon-8B on a single A100-80GB with vLLM dynamic batching, <code class="language-plaintext highlighter-rouge">n=16</code>prompts, and a 1 kbp output. We reran our Trainium2 benchmark with the same high-level workload shape: <code class="language-plaintext highlighter-rouge">n=16</code>, 1020 bp prompts, 1020 bp generated DNA per prompt, direct 6-mer generation, and greedy decoding. The comparison table below puts the published A100 reference beside our Trainium2 batch-16 run.</p>

<p>This is not a perfect accelerator shootout. The A100 baseline uses vLLM dynamic batching. Our Trainium2 run uses a static compiled NxDI batch on one <code class="language-plaintext highlighter-rouge">trn2.3xlarge</code>. The useful reading is operational: how much of the published GPU throughput can a small AWS-native Trainium2 instance recover immediately after a model release?</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>HF A100-80GB direct 6-mer</th>
      <th>HF A100-80GB FNS bp-level</th>
      <th>Our Trainium2 direct 6-mer</th>
      <th>Trainium / A100 direct</th>
      <th>Harness note</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Carbon-3B</td>
      <td>18.9 kbp/s</td>
      <td>12.3 kbp/s</td>
      <td>11.00 kbp/s</td>
      <td>58.2%</td>
      <td>A100: vLLM dynamic batch 16; Trainium2: NxDI static batch 16</td>
    </tr>
    <tr>
      <td>Carbon-8B</td>
      <td>12.0 kbp/s</td>
      <td>12.3 kbp/s</td>
      <td>8.21 kbp/s</td>
      <td>68.4%</td>
      <td>A100: vLLM dynamic batch 16; Trainium2: NxDI static batch 16</td>
    </tr>
  </tbody>
</table>

<p><em>Figure 3. Published Carbon A100-80GB throughput from the technical report compared with our Trainium2 batch-16 measurements. This is a reference comparison, not a controlled accelerator shootout.</em></p>

<p><strong>Fair reading:</strong> A100 plus vLLM is a strong optimized baseline. Trainium2 plus NxDI is the day-one AWS portability result. The important part is that Carbon-3B and Carbon-8B reached useful throughput on a single <code class="language-plaintext highlighter-rouge">trn2.3xlarge</code>, without turning the first experiment into a larger GPU deployment.</p>

<h3 id="cost-normalized-throughput">Cost-normalized throughput</h3>

<p>Throughput only becomes useful for a business when it meets the buying motion. This is where the Trainium2 shape is important. As checked on May 21, 2026, AWS lists <code class="language-plaintext highlighter-rouge">trn2.3xlarge</code> Capacity Blocks at <strong>$2.235 per instance-hour</strong> for one Trainium2 accelerator. The larger <code class="language-plaintext highlighter-rouge">trn2.48xlarge</code> keeps the same listed per-accelerator rate at <strong>$35.7608/hr</strong> for 16 Trainium2 accelerators. That is a clean path for a team: start with one 96 GB accelerator, prove the model, then scale the same Neuron/NxDI stack when the workload is real.</p>

<p>The A100 reference in the Carbon report is a single A100-80GB. On AWS, that class of capacity is commonly bought as a larger node such as <code class="language-plaintext highlighter-rouge">p4de.24xlarge</code>. The same May 21, 2026 Capacity Blocks pricing lists that node at <strong>$14.7663/hr</strong> for eight A100 accelerators. Normalizing that by eight GPUs is useful when a company already has a mature GPU fleet and can keep the whole node busy. It is less useful for a first genomics service, where the practical question is usually simpler: “What is the smallest serious accelerator unit we can reserve and defend?”</p>

<table>
  <thead>
    <tr>
      <th>Planning view</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Smallest serious start</td>
      <td>$2.235/hr</td>
    </tr>
    <tr>
      <td>Full A100 node floor</td>
      <td>$14.77/hr</td>
    </tr>
    <tr>
      <td>Carbon-3B first-service view</td>
      <td>$56 vs $217/Gbp</td>
    </tr>
    <tr>
      <td>Carbon-8B first-service view</td>
      <td>$76 vs $342/Gbp</td>
    </tr>
  </tbody>
</table>

<p>For this dollar-per-gigabase comparison, we only include Carbon-3B and Carbon-8B. Hugging Face Bio publishes the A100 reference points for those two models in the report table we used, but not for Carbon-500M. Carbon-500M is still a very useful Trainium2 result, especially for high-volume scanning and quick experiments, but adding it to the A100 comparison would make the chart look more complete than the evidence actually is. The cost table below uses the public Capacity Blocks rates above and should be read as a planning view, not a universal price/performance ranking.</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Measured bp/s on Trainium2</th>
      <th>Base pairs / hour</th>
      <th>30-day base pairs</th>
      <th>Trainium2 cost / Gbp</th>
      <th>A100 full-node floor / Gbp</th>
      <th>A100 full-utilization normalized / Gbp</th>
      <th>Mbp / dollar on Trainium2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Carbon-3B</td>
      <td>10,998</td>
      <td>39.59M</td>
      <td>28.51B</td>
      <td>$56</td>
      <td>$217</td>
      <td>$27</td>
      <td>17.7</td>
    </tr>
    <tr>
      <td>Carbon-8B</td>
      <td>8,212</td>
      <td>29.56M</td>
      <td>21.29B</td>
      <td>$76</td>
      <td>$342</td>
      <td>$43</td>
      <td>13.2</td>
    </tr>
  </tbody>
</table>

<p><em>Figure 4. Cost-normalized DNA generation throughput for Carbon-3B and Carbon-8B. Trainium2 uses AWS’s published <code class="language-plaintext highlighter-rouge">trn2.3xlarge</code> Capacity Blocks rate of $2.235/hour, checked on May 21, 2026. The A100 full-node floor assumes a <code class="language-plaintext highlighter-rouge">p4de.24xlarge</code> reservation is used for one Carbon workload. The normalized A100 line divides that node by eight GPUs and assumes perfect utilization across all accelerators. Carbon-500M is intentionally excluded because the Carbon report table used here does not publish the matching A100 reference point for that model.</em></p>

<p>The commercial reading is not “Trainium2 beats A100 in every possible view.” That would be the wrong claim. If a company already owns or reserves an A100 fleet and can keep every GPU saturated, the normalized GPU economics are strong. The same caveat applies beyond this AWS comparison: H100, H200, and GB200 capacity from AWS or specialized GPU clouds can be very competitive when the workload, discounts, availability, and utilization line up. We did not benchmark those options here. The Trainium2 win in this post is the starting shape: a one-chip AWS accelerator with 96 GB of memory, a direct path to a 16-chip Trn2 fleet, and a Neuron/NxDI stack that ran Carbon on day one. For CTOs and platform buyers, that is the difference between a model that is interesting in a report and a model that can be tested inside the real AWS environment quickly.</p>

<p><strong>What the numbers say:</strong> Carbon runs, the output is mostly clean DNA, and the throughput is already useful for early production planning. These are not final service economics, but they are enough to justify the next step: testing Carbon inside the customer’s own AWS environment.</p>

<p>One more engineering note: Carbon-3B is the cleanest fit under TP=4 because its 4 KV heads align exactly to the tensor-parallel degree. Carbon-500M and Carbon-8B trigger a GQA-to-MHA sharding fallback, but both compile and produce correct output. Across all three sizes, the plain BF16 NxDI configuration was the best measured setup. No custom kernels were needed for this first pass.</p>

<p>That leaves obvious headroom. These numbers are the baseline we got from the standard NxDI path, not the ceiling for Trainium2. The next optimization pass is where Neuron-specific work starts to matter: tighter bucketing, better serving shapes, and potentially NKI kernels for hot paths such as attention, projections, RoPE, or normalization. In other words, the current result already makes Trainium2 credible for Carbon, and the AWS stack still gives us engineering room to push the throughput and latency further.</p>

<h2 id="why-this-matters-for-bio-and-hcls">Why this matters for bio and HCLS</h2>

<p>For bio and HCLS leaders, this is not really a story about a model demo. It is a story about reducing the distance between scientific AI and a deployment path that a real enterprise can approve. A fresh open genomic model moved onto AWS-native accelerator infrastructure in a day, with numbers, caveats, and a buying shape that CTOs, platform leaders, security teams, and commercial buyers can reason about.</p>

<p>Hugging Face deserves credit for making Carbon a practical open release, not just a research announcement. Open weights, public model cards, and an inspectable path give customers more control over fine-tuning, continued pretraining, auditing, and deployment. In biology, that matters because sensitive sequence workflows cannot always be sent to a black-box endpoint.</p>

<p>AWS deserves credit for giving those teams a serious deployment lane. Trainium2 provides the accelerator. Neuron and NxD Inference provide the software layer. The AWS environment provides the security boundary, networking, IAM, procurement, and operational controls that enterprise teams already know how to use. Together, that makes open bio models much easier to move from slideware to a production-shaped proof.</p>

<p>Put Hugging Face Bio, Carbon, AWS Trainium2, and NxD Inference together, and the question changes. It is no longer “can we even run this model?” It becomes “which latency, throughput, and cost point do we want for sequence generation, variant scoring, or perturbation sweeps?” That is a much better conversation for a CTO or CEO, because it turns the model from an interesting artifact into an infrastructure decision.</p>

<p><strong>Where Loka fits:</strong> this is the bridge we care about: take a strong open model, make it run on AWS infrastructure, measure it honestly, and turn it into something a customer can evaluate with security, cost, and procurement in the room.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The core result is simple: Carbon runs on Trainium2 through NxD Inference today. Not after a model rewrite. Not after model-specific kernels. Today, with tokenizer handling, a small compatibility patch, and the existing Llama causal path.</p>

<p>That is good for Hugging Face because it shows the value of releasing open biology models in a form that infrastructure teams can actually adopt. It is good for AWS because it shows Trainium2 and NxDI can carry a fresh, domain-specific model without waiting for a long porting cycle. And it is good for bio and HCLS teams because it lowers the cost and friction of the first real deployment conversation.</p>

<p>The numbers are already useful: Carbon-500M reaches 1,568 bp/s, Carbon-3B reaches 846 bp/s, and Carbon-8B reaches 679 bp/s in the batch-1 prompt suite. In the A100-style batch-16 sweep, the same instance reaches 18.83 kbp/s, 11.00 kbp/s, and 8.21 kbp/s respectively at $2.235/hr. The caveats are real, and this is still before the deeper Trainium2 optimization work: improved serving shapes, tighter NxDI tuning, and NKI kernels can all move these numbers in the right direction. But the bigger takeaway is clear: <strong>the bottleneck for genomic AI is increasingly the gap between an open model release and a production-grade serving artifact. With Carbon, Trainium2, and NxD Inference, that gap got shorter.</strong></p>

<p>If you are building with open bio models and need them running in production on AWS, <a href="https://www.loka.com/">reach out to Loka</a>. This is exactly the problem we work on.</p>

<p><strong>Want the code-side walkthrough?</strong> We also prepared a companion engineering post that goes deeper on the tokenizer handling, NxD Inference compile path, benchmark harness, and optimization variants. Read it on <a href="https://medium.com/loka-engineering/running-hugging-faces-carbon-on-aws-trainium2-with-nxd-inference-ecdee6c2f4ce?postPublishedType=repub">Loka Engineering on Medium</a>, or inspect the benchmark package in the <a href="https://github.com/LokaHQ/carbon-neuronx-distributed-inference">public GitHub repository</a>.</p>

<p><strong>A small thank-you:</strong> Sincere thanks to Petar Kalinovski, Henrique Ribeiro Delgado da Silva, Tiago Gonçalves, João Correia, Telmo Felgueira, Pedro Dias, and Zafir Stojanovski for taking the time to review, sanity-check, and sharpen this work before it went out.</p>

<h2 id="citation">Citation</h2>

<div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@misc</span><span class="p">{</span><span class="nl">loka_carbon_trainium2_2026</span><span class="p">,</span>
  <span class="na">title</span>  <span class="p">=</span> <span class="s">{Running Hugging Face Carbon on AWS Trainium2 with NxD Inference}</span><span class="p">,</span>
  <span class="na">author</span> <span class="p">=</span> <span class="s">{Bojan Jakimovski and Loka Applied Research}</span><span class="p">,</span>
  <span class="na">year</span>   <span class="p">=</span> <span class="s">{2026}</span><span class="p">,</span>
  <span class="na">month</span>  <span class="p">=</span> <span class="s">{May}</span><span class="p">,</span>
  <span class="na">url</span>    <span class="p">=</span> <span class="s">{https://loka.com/}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="references">References</h2>

<ul>
  <li>Hugging Face Bio (2026). <em>Carbon: Open DNA Foundation Models.</em> Technical report covering the 6-mer tokenization design, Factorized Nucleotide Supervision, pretraining setup, and benchmark comparisons for all three model sizes. <a href="https://github.com/huggingface/carbon/blob/main/tech-report.pdf">github.com/huggingface/carbon</a></li>
  <li>Hugging Face Bio (2026). <em>Carbon interactive demo and technical narrative.</em> Public Carbon release page covering open weights, open code, the 6-mer tokenizer, long-context extension, throughput framing, and the live DNA generation demo. <a href="https://huggingface.co/spaces/HuggingFaceBio/carbon-demo">huggingface.co/spaces/HuggingFaceBio/carbon-demo</a></li>
  <li>Hugging Face Bio (2026). <em>Carbon-500M, Carbon-3B, and Carbon-8B model cards.</em> Open-weight checkpoints and tokenizer code on Hugging Face Hub. <a href="https://huggingface.co/HuggingFaceBio/Carbon-500M">huggingface.co/HuggingFaceBio/Carbon-500M</a> · <a href="https://huggingface.co/HuggingFaceBio/Carbon-3B">huggingface.co/HuggingFaceBio/Carbon-3B</a> · <a href="https://huggingface.co/HuggingFaceBio/Carbon-8B">huggingface.co/HuggingFaceBio/Carbon-8B</a></li>
  <li>AWS Neuron (2024). <em>NxD Inference: NeuronX Distributed Inference.</em> Open-source library for deploying large language models on AWS Trainium and Inferentia accelerators, including tensor-parallel and bucketed autoregressive serving via the NxDI Llama causal path. <a href="https://github.com/aws-neuron/neuronx-distributed-inference">github.com/aws-neuron/neuronx-distributed-inference</a></li>
  <li>Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H., &amp; Stoica, I. (2023). <em>Efficient Memory Management for Large Language Model Serving with PagedAttention.</em> Proceedings of the 29th Symposium on Operating Systems Principles (SOSP 2023). The inference engine used in the Carbon technical report’s A100-80GB baseline measurements; the direct 6-mer and FNS throughput figures compared in this post were generated with vLLM dynamic batching. <a href="https://arxiv.org/abs/2309.06180">arxiv.org/abs/2309.06180</a></li>
  <li>AWS (2024). <em>AWS Neuron SDK documentation.</em> Official documentation for compiling, profiling, and serving models on the Neuron runtime, covering both Trainium and Inferentia generations. <a href="https://awsdocs-neuron.readthedocs-hosted.com/">awsdocs-neuron.readthedocs-hosted.com</a></li>
  <li>AWS (2024). <em>AWS Trainium and Trainium2.</em> Amazon EC2 accelerator chip family purpose-built for deep learning training and inference workloads at scale. <a href="https://aws.amazon.com/ai/machine-learning/trainium/">aws.amazon.com/ai/machine-learning/trainium</a></li>
  <li>AWS (2026). <em>Applying multimodal biological foundation models across therapeutics and patient care.</em> AWS overview of BioFM use cases, AWS infrastructure layers, and implementation partners for moving BioFM proof-of-concepts toward production. <a href="https://aws.amazon.com/blogs/machine-learning/applying-multimodal-biological-foundation-models-across-therapeutics-and-patient-care/">aws.amazon.com/blogs/machine-learning</a></li>
  <li>AWS (2026). <em>Amazon EC2 Capacity Blocks for ML pricing.</em> Published per-instance pricing for reserved ML capacity. Rates used in this analysis were checked on May 21, 2026: <code class="language-plaintext highlighter-rouge">trn2.3xlarge</code> at $2.235/hr in ap-southeast-2 (Melbourne), <code class="language-plaintext highlighter-rouge">trn2.48xlarge</code> at $35.7608/hr in us-east-2 (Ohio), and <code class="language-plaintext highlighter-rouge">p4de.24xlarge</code> at $14.7663/hr in US East/US West regions where listed. <a href="https://aws.amazon.com/ec2/capacityblocks/pricing/">aws.amazon.com/ec2/capacityblocks/pricing</a></li>
  <li>AWS Neuron (2026). <em>NKI samples and NKI library kernels.</em> Public examples and references for optimized Neuron kernels, including attention, projection, RoPE, and normalization paths relevant to future Carbon optimization work. <a href="https://github.com/aws-neuron/nki-samples">github.com/aws-neuron/nki-samples</a></li>
  <li>Zhang, B., &amp; Sennrich, R. (2019). <em>Root Mean Square Layer Normalization.</em> NeurIPS 2019. Introduces RMSNorm as a computationally efficient alternative to standard layer normalization; used in Carbon’s Transformer backbone. <a href="https://arxiv.org/abs/1910.07467">arxiv.org/abs/1910.07467</a></li>
  <li>Shazeer, N. (2020). <em>GLU Variants Improve Transformer.</em> Foundational work on Gated Linear Unit variants including SwiGLU, the activation function used in Carbon’s feed-forward layers. <a href="https://arxiv.org/abs/2002.05202">arxiv.org/abs/2002.05202</a></li>
  <li>Su, J., Lu, Y., Pan, S., Murtadha, A., Wen, B., &amp; Liu, Y. (2021). <em>RoFormer: Enhanced Transformer with Rotary Position Embedding.</em> Introduces RoPE (Rotary Position Embedding), the positional encoding scheme used in Carbon’s attention layers. <a href="https://arxiv.org/abs/2104.09864">arxiv.org/abs/2104.09864</a></li>
  <li>Grattafiori, A., Dubey, A., et al. (2024). <em>The Llama 3 Herd of Models.</em> Meta AI. Introduces the Llama 3 model family and the decoder-only Transformer architecture that Carbon adopts as its backbone, enabling compatibility with standard inference runtimes including NxD Inference. <a href="https://arxiv.org/abs/2407.21783">arxiv.org/abs/2407.21783</a></li>
  <li>Ainslie, J., Lee-Thorp, J., de Jong, M., Zemlyanskiy, Y., Lebrón, F., &amp; Sanghai, S. (2023). <em>GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.</em> EMNLP 2023. Introduces Grouped-Query Attention, the attention variant used by Carbon and handled by NxDI’s GQA sharding path. <a href="https://arxiv.org/abs/2305.13245">arxiv.org/abs/2305.13245</a></li>
  <li>Nguyen, E., Poli, M., Faizi, M., Thomas, A., Birch-Sykes, C., Wornow, M., et al. (2023). <em>HyenaDNA: Long-Range Genomic Sequence Modeling at Single Nucleotide Resolution.</em> NeurIPS 2023. Long-context DNA modeling at single-nucleotide resolution; useful context for understanding the tokenization tradeoffs in Carbon’s 6-mer design. <a href="https://arxiv.org/abs/2306.15794">arxiv.org/abs/2306.15794</a></li>
  <li>Dalla-Torre, H., Gonzalez, L., Mendoza-Revilla, J., et al. (2025). <em>The Nucleotide Transformer: Building and Evaluating Robust Foundation Models for Human Genomics.</em> Nature Methods 22, 287-297 (2025). Transformer-based DNA foundation model from InstaDeep, Nvidia, and Technical University of Munich; part of the landscape of genomic sequence models that Carbon addresses with its efficiency-first 6-mer design. <a href="https://www.nature.com/articles/s41592-024-02523-z">nature.com/articles/s41592-024-02523-z</a></li>
  <li>Arc Institute (2025). <em>Evo 2: Genome-scale foundation model at single-nucleotide resolution.</em> Long-context DNA foundation model; the primary throughput and capability baseline cited in the Carbon technical report. Carbon-3B matches Evo2-7B on variant effect prediction and long-context benchmarks while being 2.3× smaller and running more than 150× faster. <a href="https://arcinstitute.org/tools/evo">arcinstitute.org/tools/evo</a></li>
  <li>National Human Genome Research Institute (NHGRI). <em>The Cost of Sequencing a Human Genome.</em> NHGRI fact sheet tracking the historical decline in human genome sequencing costs, from the ~$3 billion Human Genome Project to sub-$1,000 whole-genome sequencing; the original cost context cited in the introduction. <a href="https://www.genome.gov/about-genomics/fact-sheets/Sequencing-Human-Genome-cost">genome.gov/about-genomics/fact-sheets/Sequencing-Human-Genome-cost</a></li>
</ul>]]></content><author><name>shekswess</name></author><category term="AI" /><category term="LLM" /><category term="AWS Trainium2" /><category term="NxD Inference" /><category term="Neuron" /><category term="Hugging Face" /><category term="Carbon" /><category term="Bio AI" /><category term="Genomics" /><summary type="html"><![CDATA[A first NxD Inference benchmark for open DNA foundation models.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://lokahq.github.io/carbon-neuronx-distributed-inference/assets/carbon-trainium2-cover.png" /><media:content medium="image" url="https://lokahq.github.io/carbon-neuronx-distributed-inference/assets/carbon-trainium2-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Agent Assembly Line</title><link href="https://shekswess.github.io/the-agent-assembly-line.html" rel="alternate" type="text/html" title="The Agent Assembly Line" /><published>2026-03-01T16:00:00+00:00</published><updated>2026-03-01T16:00:00+00:00</updated><id>https://shekswess.github.io/the-agent-assembly-line</id><content type="html" xml:base="https://shekswess.github.io/the-agent-assembly-line.html"><![CDATA[<p><em><strong>Original Source of the blog post: <a href="https://medium.com/loka-engineering/the-agent-assembly-line-eabc3e95263d">The Agent Assembly Line</a></strong></em></p>

<p><em><strong>Written by Mario Petkoski, Bojan Jakimovski, and Zafir Stojanovski</strong></em></p>

<p>Every team building with LLMs hits the same wall. The demo works in a notebook. Then you spend three months wiring up config loading, session persistence, streaming APIs, guardrails, evaluation, and deployment before a single user touches it.</p>

<p>We kept hitting that wall across projects. So we built an internal template that standardizes the 80% of agent infrastructure that never changes, so each new agent becomes YAML + prompt + tools + eval instead of a bespoke integration project.</p>

<p>Our stack:</p>

<ul>
  <li><strong>Strands Agents</strong>: runtime</li>
  <li><strong>Amazon Bedrock</strong>: model provider</li>
  <li><strong>FastAPI / Amazon Bedrock AgentCore</strong>: service layer</li>
</ul>

<p>This post walks through the design decisions, what we automated, and the lessons we learned along the way.</p>

<h2 id="the-problem-is-never-the-agent-logic">The Problem Is Never the Agent Logic</h2>

<p>Building the agent itself, picking the model, writing the prompt, connecting some tools, that part takes a day. What takes months is everything around it. Config schemas that don’t drift between environments. Prompt loading that supports templating without becoming its own framework. Tool wiring that handles MCP servers, catalog tools, and custom Python tools in a single pass. Session persistence that works the same locally and in production. A streaming API that surfaces tool activity, not just tokens.</p>

<p>And then there’s the layer nobody wants to build but everyone needs: guardrails you can actually observe, not just toggle. An evaluation harness that catches when a prompt tweak quietly breaks something downstream. Async execution that keeps requests non-blocking so users aren’t staring at a spinner while the model reasons and tools run. A stateless API with pluggable session backends so you can scale horizontally without rewriting agent logic. A deployment path that doesn’t live in a 40 step wiki. We solved all of this by turning it into a template. The infrastructure is fixed, and you just bring the domain knowledge.</p>

<h2 id="how-we-structured-it">How We Structured It</h2>

<p>To keep things concrete, we’ll use the first agent we built with the template: an AWS documentation assistant. It answers questions about AWS services by calling an AWS Documentation MCP server. The use case is boring on purpose, but it keeps the focus on the infrastructure patterns rather than the agent’s domain logic.</p>

<p>Here’s how the pieces fit together:</p>

<p><img src="https://miro.medium.com/v2/resize:fit:1100/format:webp/1*NwAwxGwgH_jubhEldw4a2g.png" alt="Architectural Diagram for Deploying Strands Agents with Amazon Bedrock on AWS" /></p>

<p><em>Architectural Diagram for Deploying Strands Agents with Amazon Bedrock on AWS</em></p>

<p>And the delivery loop for each new use case:</p>

<p><img src="https://miro.medium.com/v2/resize:fit:1100/format:webp/1*Uwb9MUX51II2UBhaLO6O0w.png" alt="Standardized Agent Delivery Loop" /></p>

<p><em>Standardized Agent Delivery Loop</em></p>

<p>The template already provides the service surface, hardening, evaluation, and deployment layers. Each new agent is mostly: write a config, write a prompt, pick your tools.</p>

<h2 id="agents-are-config-not-code">Agents Are Config, Not Code</h2>

<p>The most impactful design decision was making agent definition purely declarative. Every agent is described by two files: a YAML config and a prompt file. No subclassing, no framework boilerplate. Here’s a trimmed version of what an agent config looks like:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">aws_documentation_agent"</span>
<span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">An</span><span class="nv"> </span><span class="s">agent</span><span class="nv"> </span><span class="s">that</span><span class="nv"> </span><span class="s">helps</span><span class="nv"> </span><span class="s">users</span><span class="nv"> </span><span class="s">find</span><span class="nv"> </span><span class="s">information</span><span class="nv"> </span><span class="s">in</span><span class="nv"> </span><span class="s">AWS</span><span class="nv"> </span><span class="s">documentation."</span>
<span class="na">model_configuration</span><span class="pi">:</span>
  <span class="na">model_id</span><span class="pi">:</span> <span class="s2">"</span><span class="s">amazon.nova-2-lite-v1:0"</span>
  <span class="na">temperature</span><span class="pi">:</span> <span class="m">0.4</span>
  <span class="na">max_tokens</span><span class="pi">:</span> <span class="m">8192</span>
<span class="na">session_manager_configuration</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s2">"</span><span class="s">s3"</span>
  <span class="na">agent_prefix</span><span class="pi">:</span> <span class="s2">"</span><span class="s">aws_documentation"</span>
<span class="na">mcp_server_configuration</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">type_server</span><span class="pi">:</span> <span class="s2">"</span><span class="s">stdio"</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s2">"</span><span class="s">uvx"</span>
    <span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">awslabs.aws-documentation-mcp-server@latest"</span><span class="pi">]</span>
<span class="na">tool_configuration</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s2">"</span><span class="s">strands-agents-tool"</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">http_request"</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s2">"</span><span class="s">self-created-tool"</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">custom_documentation"</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s2">"</span><span class="s">self-created-tool"</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">execute_python"</span>
</code></pre></div></div>

<p>The matching prompt file is intentionally tiny:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;instruction&gt;</span>
You are an AWS documentation specialist. Your job is to provide accurate, sourced answers about AWS services, APIs, and best practices by consulting official documentation.
## Tool Usage
- **AWS Documentation MCP tools**: Primary source for all AWS questions. Always search or fetch official docs before answering.
- **http_request**: Use to fetch supplementary content (e.g., AWS blog posts, GitHub examples) when official docs are insufficient.
- **execute_python**: Use to validate code or demonstrate SDK usage. Always show code and output together.
## Response Guidelines
1. **Ground answers in documentation.** Cite the official doc URL or section. If you can't find a source, say so explicitly.
2. **Be precise about service, region, and API version** when these affect the answer.
3. **Prefer concise, structured answers.** Use bullets for procedures, prose for concepts.
4. **Distinguish configuration from best practice** when they differ.
5. **Acknowledge limits.** Direct billing or service health questions to AWS Support or the Health Dashboard.
## Behavior
- Do not fabricate API parameters, ARN formats, IAM policy syntax, or service limits.
- When a user provides code or config, identify the specific AWS API or resource type before diagnosing.
- If a question is ambiguous, ask one clarifying question before diving in.
<span class="nt">&lt;/instruction&gt;</span>
</code></pre></div></div>

<p>This minimalism is deliberate. We found that starting with a small prompt and adding tools and constraints first leads to more predictable agents than front-loading prompt complexity. Evaluation catches the moment when “more prompt” starts creating more bugs than it solves.</p>

<p>The config loader supports environment variable expansion and per-environment overrides (<code class="language-plaintext highlighter-rouge">dev/</code>, <code class="language-plaintext highlighter-rouge">prod/</code> directories) so the same agent definition works across environments without code changes. Prompts support Jinja2 templating when you need it, but the default path is just a plain text file.</p>

<h2 id="tools-as-a-composition-system">Tools as a Composition System</h2>

<p>Tool integration churn is one of the biggest time sinks in agent projects. Every new capability means new wiring code, new error handling, new plumbing.</p>

<p>We solved this by treating tools as a pluggable composition system with three sources that merge into a single tool list at runtime:</p>

<ul>
  <li><strong>MCP server tools</strong>: Any MCP-compatible server, connected via config. Swapping tool providers requires zero code changes.
Example: AWS Documentation MCP Server. This MCP server provides tools to access AWS documentation, search for content, and get recommendations.</li>
  <li><strong>Strands catalog tools</strong>: Pre-built tools from the Strands ecosystem, developed by the community.
Example: <code class="language-plaintext highlighter-rouge">http_request</code> tool for API calls, fetching web data, and sending data to external services.</li>
  <li><strong>Custom Python tools</strong>: Domain-specific tools we write ourselves.
Example: a Python function.</li>
</ul>

<p>All three are declared in YAML and composed at startup. The runtime iterates through MCP clients, loads catalog modules, imports custom tools, and hands the merged list to the Strands Agent. The key insight: you can swap tool sources, build per-environment tool inventories, and observe tool usage during streaming, and all without touching runtime code. This is what makes it an “assembly line.” Adding a new capability to an agent is a YAML change, not a pull request into the agent runtime.</p>

<p>One interesting addition worth noting: AgentCore Code Interpreter. It gives agents the ability to execute arbitrary Python in a secure, managed sandbox. This enables on-the-fly calculations, data analysis, or generating visualizations mid-conversation. Because it ships as part of <code class="language-plaintext highlighter-rouge">strands-agents-tools</code>, it fits right into the same composition system. By just adding this to YAML, the agent can now write and run code without any changes to the runtime.</p>

<h2 id="a-real-api-surface-not-just-a-script">A Real API Surface, Not Just a Script</h2>

<p>We wrapped the agent in a FastAPI service layer that stays intentionally thin. FastAPI provides a consistent transport boundary for request validation, SSE streaming, response shaping, and operational endpoints, while agent orchestration, tool execution, and memory behavior remain in the runtime layer.</p>

<p>Because the <code class="language-plaintext highlighter-rouge">/invocations</code> interface is stable, the same request and streaming response model works across all deployment modes: local development, a standard deployed FastAPI service, and a Bedrock AgentCore Runtime deployment that forwards calls to the same endpoint. This keeps behavior consistent across local, self-managed, and managed environments.</p>

<p>It handles five concerns:</p>

<ul>
  <li><strong>Streaming conversations (<code class="language-plaintext highlighter-rouge">POST /invocations</code>)</strong>: SSE stream with tokens, tool activity updates, guardrail events, and a final structured payload.</li>
  <li><strong>Structured output (<code class="language-plaintext highlighter-rouge">POST /chat/structured</code>)</strong>: Schema-constrained responses validated against a JSON schema the caller provides.</li>
  <li><strong>Session browsing (<code class="language-plaintext highlighter-rouge">GET /sessions</code>)</strong>: Inspect stored transcripts for debugging or UX continuity.</li>
  <li><strong>Agent metadata (<code class="language-plaintext highlighter-rouge">GET /agent</code>)</strong>: Introspect the active configuration at runtime.</li>
  <li><strong>Health (<code class="language-plaintext highlighter-rouge">GET /ping</code>)</strong>: Standard liveness probe.</li>
</ul>

<p>The streaming design deserves a closer look. Most agent APIs stream tokens. Ours streams structured events:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>data: {"type":"token","data":"Amazon S3 provides..."}
data: {"type":"tool_update","tool":{"name":"http_request","status":"running"}}
data: {"type":"tool_update","tool":{"name":"http_request","status":"success"}}
data: {"type":"final","output_text":"...","tool_calls":[...],"guardrail_events":[]}
event: end
data: {}
</code></pre></div></div>

<p>This turned out to be one of the highest-leverage decisions in the whole template. You can debug agent runs without tailing logs. Users see that something is happening when tools are executing. And frontends have a consistent event vocabulary to build around: tool spinners, guardrail warnings, and structured final payloads all come through the same stream.</p>

<h2 id="production-hardening-we-built-in-from-day-one">Production Hardening We Built In From Day One</h2>

<p>We’re not going to tell you to “add observability later”. We built these into the template because we’ve been burned by adding them as afterthoughts.</p>

<h3 id="sessions-and-memory">Sessions and Memory</h3>

<p>We support three session backends, all config-driven: S3 (default for the server), local file (great for CLI and eval workflows), and AgentCore Memory (for retrieval-augmented session context). The difference is just where the data lands. A local directory structure for development, an S3 bucket for production, or a managed memory service for richer retrieval.</p>

<p>These backends cover short-term memory, the running conversation within a session. Context management uses a sliding window with configurable size and truncation behavior. When the window overflows, the oldest messages are trimmed automatically. For cases where you want to preserve context rather than discard it, there is also a summarizing conversation manager that condenses older messages into structured summaries instead of dropping them entirely.</p>

<p>Long-term memory goes a step further. Instead of replaying raw message history, it extracts durable knowledge across sessions: factual information, learned user preferences, and compressed session summaries. This means an agent can greet a returning user already knowing their portfolio focus, their preferred analysis style, or what they asked about last week, without stuffing the entire conversation history into the context window.</p>

<h3 id="guardrails-with-two-modes">Guardrails with Two Modes</h3>

<p>We wired Bedrock Guardrails through a hook system that intercepts both input and output. In <code class="language-plaintext highlighter-rouge">detect</code> mode, violations are surfaced as streaming events so you can monitor without blocking users. In <code class="language-plaintext highlighter-rouge">block</code> mode, the agent stops and returns a structured intervention payload. Both modes are observable in the SSE stream. This matters because safety behavior becomes testable and auditable, not hidden behind opaque provider errors.</p>

<h3 id="observability-as-a-feature-toggle">Observability as a Feature Toggle</h3>

<p>When tracing credentials are present, the template automatically initializes OTel-based tracing tagged with user ID, session ID, and agent name. No separate instrumentation project, it’s either on or off. Langfuse works today with zero code changes, just environment variables. MLflow, Amazon AgentCore Observability, or any other OTLP-compatible backend can be swapped in with minimal adjustments. All three receive the same traces, spans, and metadata once connected.</p>

<h3 id="evaluation-baked-in">Evaluation Baked In</h3>

<p>Agents are non-deterministic. The same prompt change that improves accuracy on one query can introduce hallucinations on another, or subtly shift behavior in ways that only surface weeks later. Without a way to measure these tradeoffs before they reach users, every change is a gamble. We built an LLM-as-a-judge evaluation harness that runs against a dataset, scores agent responses across accuracy, relevance, completeness, and clarity, and produces a JSON report we can diff across runs. It runs as a single CLI command, so there is no excuse to skip it.</p>

<p>But the real value is not the scoring itself, it is the iteration loop it enables. When you change a system prompt, swap a model, add a tool, or adjust a conversation manager, you re-run the eval suite and immediately see what improved, what regressed, and what broke. If a prompt revision introduces bias, if a new tool changes how the agent frames its responses, if an edge case starts producing unsafe output, it shows up as a score delta you can inspect before it ever reaches production. This turns agent development from “deploy and hope” into a measurable engineering process where every design decision is backed by evidence rather than intuition.</p>

<h2 id="user-interface">User Interface</h2>

<p>We ship a single-file chat UI served directly by FastAPI. No build step, no framework. We use one HTML file that consumes the same SSE event stream our application would.</p>

<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*xcMBXF0WBWBE8HxngJNPGw.gif" alt="Agent Template Chat Interface (Streaming Mode)" /></p>

<p><em>Agent Template Chat Interface (Streaming Mode)</em></p>

<p>The conversation view streams live. Tokens appear as they arrive. Tool calls surface inline as status chips showing which tool is running, when it completes, whether it succeeded or errored, attached to the assistant turn that triggered them. Guardrail events show up in the stream too, so we can watch detect-mode violations without digging through logs.</p>

<p>The value isn’t the UI itself. It’s that we’re watching the real agent, on real infrastructure, with real tool calls through the exact same streaming contract our consumers will use. That makes it useful for prompt iteration, debugging unexpected tool sequences, and having something to put on screen during a demo instead of curling JSON in a terminal.</p>

<h2 id="deployment-a-paved-road-on-aws">Deployment: A Paved Road on AWS</h2>

<p>We also built a CDK-based infrastructure layer that creates a complete AWS landing zone for agent services:</p>

<ul>
  <li><strong>Infra stack</strong>: VPC, S3 bucket, ECR repo, AgentCore Runtime, AgentCore Observability, AgentCore Memory, Secrets Manager entries, GitHub Actions OIDC role for CI.</li>
  <li><strong>Compute stack</strong>: ECS Fargate behind an ALB, Route53 integration, Bedrock and bucket IAM policies.</li>
</ul>

<p>The deployment sequence is six steps: deploy infra, push container, deploy compute, inject env vars, smoke test, establish an eval cadence. The service itself runs anywhere containers run, but having a decision-free AWS path saved us significant time on our first few deployments.</p>

<h2 id="the-takeaway">The Takeaway</h2>

<p>The gap between “agent demo” and “agent in production” is real, but it’s not mysterious. It’s config management, API surfaces, session persistence, safety hooks, evaluation, and deployment automation. None of it is novel. All of it takes time if you build it from scratch for every project.</p>

<p>Our approach was to solve it once as a template, then treat each new agent as a configuration problem: YAML, a prompt file, a tool list, and an eval dataset. The runtime, the service layer, the hardening, and the deployment path stay fixed.</p>

<p>For multi-agent systems in this template, the best pattern is a supervisor (agent orchestrator) that delegates tasks to specialized sub-agents exposed as tools, so orchestration stays centralized while reusing the same config, memory, guardrails, and API surface.</p>

<p>If you’re building agent systems on AWS with Strands and Bedrock, we hope the patterns described here save you some of the months we spent figuring them out.</p>]]></content><author><name>shekswess</name></author><category term="AI" /><category term="LLM" /><category term="Agents" /><category term="AI Agent" /><category term="Amazon Bedrock" /><category term="Bedrock AgentCore" /><category term="FastAPI" /><category term="Strands Agents" /><category term="Observability" /><category term="Evaluation" /><category term="AWS" /><summary type="html"><![CDATA[Productionizing Agentic Use Cases in Weeks, Not Months]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://miro.medium.com/v2/resize:fit:1100/format:webp/1*xEcRIU_nbeSKahsPKix_kg.png" /><media:content medium="image" url="https://miro.medium.com/v2/resize:fit:1100/format:webp/1*xEcRIU_nbeSKahsPKix_kg.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Deploying Trinity-Mini-DrugProt-Think on Amazon SageMaker AI</title><link href="https://shekswess.github.io/deploying-trinity-mini-drugprot-think-on-amazon-sagemaker-ai.html" rel="alternate" type="text/html" title="Deploying Trinity-Mini-DrugProt-Think on Amazon SageMaker AI" /><published>2026-02-22T16:00:00+00:00</published><updated>2026-02-22T16:00:00+00:00</updated><id>https://shekswess.github.io/deploying-trinity-mini-drugprot-think-on-amazon-sagemaker-ai</id><content type="html" xml:base="https://shekswess.github.io/deploying-trinity-mini-drugprot-think-on-amazon-sagemaker-ai.html"><![CDATA[<p><em><strong>Original Source of the blog post: <a href="https://medium.com/loka-engineering/deploying-trinity-mini-drugprot-think-on-amazon-sagemaker-ai-9e1c1c430ce9">Deploying Trinity-Mini-DrugProt-Think on Amazon SageMaker AI</a></strong></em></p>

<p><em><strong>Written by Bojan Jakimovski, Machine Learning Lead and Petar Kalinovski, Machine Learning Engineer</strong></em></p>

<p>If you work in regulated domains (Healthcare, Life Sciences, Finance) you routinely hit constraints that break the default “just call a hosted API” approach:</p>

<ul>
  <li>You can’t send sensitive data to a third-party endpoint.</li>
  <li>You need predictable costs (not per-token pricing surprises).</li>
  <li>You need to <em>change the model</em> (domain behavior, schemas, format constraints), not just prompt harder.</li>
</ul>

<p>There’s also a bigger ecosystem dynamic: the most competitive open-weight releases have increasingly come from a small set of labs, and “who ships open weights” shapes who gets to build locally and cheaply. We chose <a href="https://www.arcee.ai">Arcee AI</a>’s <a href="https://huggingface.co/arcee-ai/Trinity-Mini">Trinity Mini</a> as a concrete Western open-weight test case, and <a href="https://biocreative.bioinformatics.udel.edu/tasks/biocreative-vii/track-1/">DrugProt</a> as the biomedical task.</p>

<p>In our <a href="https://lokahq.github.io/Trinity-Mini-DrugProt-Think/">main write-up</a> we trained <strong>Trinity-Mini-DrugProt-Think</strong>: a LoRA adapter on top of <strong>Arcee Trinity Mini</strong> for DrugProt drug-protein relation extraction via RLVR. This post is the missing “last mile”: how to serve that <strong>base model + LoRA adapter</strong> as a <strong>SageMaker real-time endpoint</strong>, using the <strong>SageMaker Python SDK v3</strong> and a standard Hugging Face PyTorch inference container.</p>

<p>The guiding principle is production leverage: ship a tiny adapter artifact, keep the base model immutable, and let SageMaker AI own the undifferentiated heavy lifting (instances, endpoint lifecycle, IAM, logs/metrics).</p>

<h3 id="what-were-deploying">What we’re deploying</h3>
<ul>
  <li><strong>Base model</strong>: <em>arcee-ai/Trinity-Mini</em></li>
  <li><strong>Fine-tune artifact</strong>: a <strong>PEFT LoRA adapter</strong> <em>Loka/Trinity-Mini-DrugProt-Think</em></li>
  <li><strong>Runtime</strong>: SageMaker AI’s real-time endpoint running a Hugging Face PyTorch inference DLC + TorchServe</li>
</ul>

<p>Why LoRA here:</p>

<ul>
  <li>The adapter is small (fast iteration, easy rollback).</li>
  <li>You can promote the same base model across environments (dev/stage/prod) and vary only adapters.</li>
  <li>You avoid “mystery merges” and can keep provenance clean.</li>
</ul>

<h3 id="why-sagemaker-ai-for-this-vs-just-use-an-api">Why SageMaker AI for this (vs “just use an API”)</h3>
<ul>
  <li><strong>Data control</strong>: keep inference inside your AWS boundary (VPC, IAM, logs, encryption) instead of sending data to a third-party endpoint.</li>
  <li><strong>Operational control</strong>: you own the container, the dependencies, the model loading path, and the request/response contract.</li>
  <li><strong>Production knobs</strong>: endpoint lifecycle, scaling policies, instance selection, and CloudWatch integration are first-class.</li>
</ul>

<p>The tradeoff is obvious: you’re now responsible for model cold starts, GPU sizing, and endpoint cost hygiene. This guide optimizes for correctness and clarity, not maximal throughput.</p>

<p>Knowing the pros and the cons of SageMaker AI, let’s see how to actually deploy the model!</p>

<h3 id="architecture-and-inference-logic">Architecture and Inference Logic</h3>
<p><img src="https://cdn-images-1.medium.com/max/1024/1*kr2yGEjTGATqOKDhsZrkvQ.png" alt="Architecture Diagram" /></p>

<p>Architectural Diagram for Deployment of Arcee’s Trinity Models (or Open-Source Models) on Amazon SageMaker AI</p>

<p>The architecture diagram for the components needed for deploying Open-Weight models is pretty simple, because most of the painful infrastructure side is handled by SageMaker AI, especially when working with v3 of the SageMaker AI’s SDK. In the repository we share 3 important scripts that handle different things:</p>

<ol>
  <li><em>serving/lora_inference/spec.py</em> implements an SDK v3 <em>InferenceSpec</em> and TorchServe-compatible handler (<em>model_fn</em>/<em>predict_fn</em>).</li>
  <li><em>serving/scripts/deploy_lora_endpoint.py</em> packages either: a local adapter directory into the model artifact under <em>adapter/</em>, or an empty model artifact while pointing the container at a remote adapter repo id.</li>
  <li>SageMaker creates/updates a real-time endpoint with the container + env vars.</li>
</ol>

<p>What’s “v3” about it: instead of hand-assembling <em>Model</em> + <em>EndpointConfig</em> objects, we use <em>ModelBuilder</em> + <em>InferenceSpec</em> to define <em>exactly</em> how the container loads and serves the model, with less SageMaker boilerplate and a tighter local-to-prod story.</p>

<p>At runtime:</p>

<ul>
  <li>The container downloads the base model + adapter (unless already cached on the instance volume).</li>
  <li><em>PeftModel.from_pretrained(…)</em> attaches the adapter to the base model.</li>
  <li>Requests are served via <em>transformers.generate(…)</em>.</li>
</ul>

<h3 id="prerequisites">Prerequisites</h3>
<ol>
  <li>
    <p><strong>AWS account</strong> + credentials on the machine running the deploy script (local or CI runner).</p>
  </li>
  <li>
    <p>A <strong>SageMaker AI execution role ARN</strong> with permissions for: SageMaker AI for model/endpoint operations (scope down in real prod), ECR pull for the inference DLC image, CloudWatch logs, and optional S3 if you later move artifacts there.</p>
  </li>
  <li>
    <p><strong>Service quota</strong> for your chosen instance type in your region.</p>
  </li>
  <li>
    <p>Optional: <em><code class="language-plaintext highlighter-rouge">HF_TOKEN</code></em> if your <strong>base model</strong> or <strong>adapter repo</strong> is private or gated.</p>
  </li>
</ol>

<p>After these initial prerequisites, the next step is to clone the awesome repository containing all the scripts necessary for these operations:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/LokaHQ/Trinity-Mini-DrugProt-Think.git
</code></pre></div></div>

<p>Now you would need to install dependencies (check if you previously have uv installed):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv <span class="nb">sync</span>
</code></pre></div></div>

<p>Then you configure the AWS credentials (if you’re running locally):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws configure
</code></pre></div></div>

<p>If you prefer SageMaker AI Studio, you can run the same commands inside a Studio environment with an attached execution role (no local AWS config required). Now let’s start with the deployment!</p>

<h3 id="deploying-lora-adapter-from-hugging-face">Deploying LoRA Adapter from Hugging Face</h3>
<p>Firsly let’s check how the deployment process works if you get the LoRA Adapter from Hugging Face repository:</p>

<ol>
  <li>First set the execution role:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export SAGEMAKER_ROLE_ARN="arn:aws:iam::&lt;account-id&gt;:role/&lt;sagemaker-execution-role&gt;"
export HF_TOKEN="hf_xxx" # optional
</code></pre></div></div>

<ol>
  <li>After the execution role is set, you can deploy the model by executing the script:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python serving/scripts/deploy_lora_endpoint.py \
 --endpoint-name trinity-mini-drugprot-think \
 --adapter-id &lt;org-or-user&gt;/&lt;adapter-repo&gt; \
 --role-arn "$SAGEMAKER_ROLE_ARN" \
 --instance-type ml.p4d.24xlarge \
 --region us-west-2
</code></pre></div></div>

<p>Notes you should not skip:</p>

<ul>
  <li>The script defaults to a <strong>us-west-2</strong> Hugging Face DLC image URI. If you deploy to another region, pass a region-appropriate –<em>image-uri</em> (or keep <em>region us-west-2</em>)</li>
  <li>First deployment is dominated by <strong>cold-start downloads</strong> (base model + adapter). Plan your timeouts accordingly (– <em>model-server-timeout</em>).</li>
  <li><strong>Instance sizing is real</strong>: Trinity Mini is a sparse MoE, but you still need to load the <em>full</em> checkpoint. If you try to run this on a small single-GPU instance, <em>device_map=auto</em> may spill to CPU and “work”, but latency will be ugly. Pick hardware based on your SLOs, not vibes.</li>
</ul>

<h4 id="deploying-lora-adapter-from-local-machines3">Deploying LoRA Adapter from Local Machine/S3</h4>

<p>If you have an adapter folder locally (containing <em>adapter_config.json</em> and adapter weights), you can deploy it directly:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python serving/scripts/deploy_lora_endpoint.py \
 --endpoint-name trinity-mini-drugprot-think-local \
 --adapter-id ./adapter \
 --role-arn "$SAGEMAKER_ROLE_ARN" \
 --instance-type ml.p4d.24xlarge \
 --region us-west-2
</code></pre></div></div>

<p>If <em>./adapter/adapter_config.json</em> includes <em>base_model_name_or_path</em>, the server can resolve the base model automatically; otherwise override with:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python serving/scripts/deploy_lora_endpoint.py \
 --base-model-id arcee-ai/Trinity-Mini \
 --endpoint-name trinity-mini-drugprot-think-local \
 --adapter-id ./adapter \
 --role-arn "$SAGEMAKER_ROLE_ARN" \
 --instance-type ml.p4d.24xlarge \
 --region us-west-2
</code></pre></div></div>

<h3 id="update-an-existing-endpoint">Update an existing endpoint</h3>
<p>Let’s say you have one version of the model and you would want to update it easily with another LoRA adapter, then this will swap the model behind the same endpoint name:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python serving/scripts/deploy_lora_endpoint.py \
 --endpoint-name trinity-mini-drugprot-think \
 --adapter-id &lt;org-or-user&gt;/&lt;adapter-repo&gt; \
 --role-arn "$SAGEMAKER_ROLE_ARN" \
 --update-endpoint
</code></pre></div></div>

<p>This is the right workflow for:</p>

<ul>
  <li>promoting a new adapter revision,</li>
  <li>rolling back safely,</li>
  <li>keeping client integration stable.</li>
</ul>

<h3 id="invoke-the-endpoint">Invoke the endpoint</h3>
<p>To test the deployed endpoint, there is a prepared script that makes the testing inference straightforward:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python serving/scripts/test_lora_endpoint.py \
 --endpoint-name trinity-mini-drugprot-think \
 --prompt "Extract the DrugProt relation type(s) from: 'Imatinib inhibits ABL1 kinase.' Return a short label." \
 --max-new-tokens 64 \
 --temperature 0.0 \
 --top-p 1.0 \
 --no-do-sample
</code></pre></div></div>

<h3 id="payload-examples">Payload examples</h3>
<p>This is how the request and the response for that request should look like:</p>

<ul>
  <li>Request:</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "inputs": "string prompt",
  "max_new_tokens": 2048,
  "temperature": 0.7,
  "top_p": 0.95,
  "do_sample": true
}
</code></pre></div></div>

<ul>
  <li>Response:</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "generated_text": "…",
  "full_text": "…",
  "model_id": "arcee-ai/Trinity-Mini",
  "adapter_id": "&lt;adapter path or repo id&gt;",
  "model_name": "Trinity-Mini-DrugProt-Think"
}
</code></pre></div></div>

<h3 id="operational-notes">Operational notes</h3>
<p>There are some important operational notes when going into full production mode:</p>

<ul>
  <li><strong>Latency/throughput</strong>: this implementation uses <em>transformers.generate(…)</em> inside TorchServe. It’s correct and simple, but not a throughput monster. If you need high QPS / high token/sec, consider a vLLM/TGI/DJL-based container and treat this endpoint as the reference implementation.</li>
  <li><strong>Cold start</strong>: downloading base+adapter at container start is easy but slow. For strict SLOs, pre-package artifacts (S3 model tarball / private ECR image) so workers start hot.</li>
  <li><strong>Security</strong>: if you run in a VPC with egress restricted, Hugging Face downloads will fail unless you allow outbound or pre-stage artifacts. For private adapters, <em>HF_TOKEN</em> must be provided via secure env/secret injection.</li>
  <li><strong>Timeouts</strong>: <code class="language-plaintext highlighter-rouge">--model-server-timeout</code> controls both TorchServe and MMS timeouts (see <em>SAGEMAKER_MODEL_SERVER_TIMEOUT</em>, <em>MMS_DEFAULT_</em> in the deploy script).</li>
  <li><strong>Dependency drift</strong>: the container runtime installs <em>serving/lora_inference/requirements.txt</em> (pins <em>transformers==4.57.3</em>, <em>peft==0.14.0 for our Arcee Models</em>). Keep this aligned with your training/export stack to avoid subtle adapter loading issues.</li>
  <li><strong>SDK v3 footgun</strong>: <em>serving/scripts/deploy_lora_endpoint.py</em> includes a targeted workaround for an SDK v3 repack/upload path edge case when combining <em>model_path</em> + <em>source_code</em>. If you copy-paste the deploy script into another repo, keep that patch or validate the SDK version you’re on.</li>
</ul>

<h3 id="cleanup">Cleanup</h3>
<p>It is important after every experimentation with different setups to cleanup models that are still deployed behind endpoints, but are not used. That can be done using:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python serving/scripts/delete_lora_endpoint.py \
 --endpoint-name trinity-mini-drugprot-think \
 --region us-west-2
</code></pre></div></div>

<h3 id="conclusion">Conclusion</h3>
<p>If you’ve made it this far, you now have a clean, reproducible path from an RLVR-trained LoRA adapter to a managed production endpoint: <strong>base model stays immutable</strong>, <strong>adapters iterate quickly</strong>, and <strong>SageMaker AI owns the operational surface area</strong> (endpoint lifecycle, IAM integration, logs/metrics).</p>

<p>The main decisions that determine whether this feels “production-grade” are not in the Python code: they’re <strong>instance sizing</strong>, <strong>cold-start strategy</strong> (download vs pre-packaged artifacts), and whether TorchServe + <em>transformers.generate</em> is sufficient for your <strong>throughput/SLO</strong>. If you outgrow this reference stack, keep the same adapter/base split and move the serving layer to a higher-throughput runtime (vLLM/TGI/DJL) while preserving the request/response contract.</p>

<p>Production-grade ML is not about bigger models - it is about clearer boundaries, reproducible artifacts, and operational control. And yes, production is where the real fun starts.</p>

<h3 id="additional-helpful-links">Additional Helpful Links</h3>
<ul>
  <li><a href="https://lokahq.github.io/Trinity-Mini-DrugProt-Think/">Experiments Technical Report</a></li>
  <li><a href="https://github.com/LokaHQ/Trinity-Mini-DrugProt-Think">Code Repository</a></li>
  <li><a href="https://huggingface.co/lokahq/Trinity-Mini-DrugProt-Think">HF Adapters</a></li>
  <li><a href="https://github.com/aws/sagemaker-python-sdk">SageMaker Python SDK v3</a></li>
  <li><a href="https://sagemaker.readthedocs.io/en/stable/">SageMaker Python SDK docs</a></li>
  <li><a href="https://docs.aws.amazon.com/sagemaker/latest/dg/realtime-endpoints.html">Real-time endpoints</a></li>
  <li><a href="https://aws.amazon.com/account/">AWS account</a></li>
  <li><a href="https://aws.amazon.com/cli/">AWS CLI</a></li>
  <li><a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html">IAM roles</a></li>
  <li><a href="https://docs.aws.amazon.com/servicequotas/latest/userguide/intro.html">Service Quotas</a></li>
</ul>]]></content><author><name>shekswess</name></author><category term="AI" /><category term="LLM" /><category term="Amazon SageMaker" /><category term="AI" /><category term="LLM" /><category term="SDK v3" /><category term="Deployment" /><category term="Arcee" /><category term="Trinity-Mini-DrugProt-Think" /><summary type="html"><![CDATA[From Experiments to Production with SageMaker AI's SDK v3]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://miro.medium.com/v2/resize:fit:1100/format:webp/1*hGTTkxr2NSYWjqvFpVBxAA.png" /><media:content medium="image" url="https://miro.medium.com/v2/resize:fit:1100/format:webp/1*hGTTkxr2NSYWjqvFpVBxAA.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Post-Training an Open MoE Model to Extract Drug-Protein Relations: Trinity-Mini-DrugProt-Think</title><link href="https://shekswess.github.io/post-training-an-open-moe-model-to-extract-drug-protein-relations-trinity-mini-drugprot-think.html" rel="alternate" type="text/html" title="Post-Training an Open MoE Model to Extract Drug-Protein Relations: Trinity-Mini-DrugProt-Think" /><published>2026-02-22T16:00:00+00:00</published><updated>2026-02-22T16:00:00+00:00</updated><id>https://shekswess.github.io/post-training-an-open-moe-model-to-extract-drug-protein-relations-trinity-mini-drugprot-think</id><content type="html" xml:base="https://shekswess.github.io/post-training-an-open-moe-model-to-extract-drug-protein-relations-trinity-mini-drugprot-think.html"><![CDATA[<p><em><strong>Original Source of the blog post: <a href="https://lokahq.github.io/Trinity-Mini-DrugProt-Think/">Post-Training an Open MoE Model to Extract Drug-Protein Relations: Trinity-Mini-DrugProt-Think</a></strong></em></p>

<p><em><strong>Written by Bojan Jakimovski and Petar Kalinovski</strong></em></p>

<h2 id="introduction">Introduction</h2>

<p>In the past two years,
Chinese labs have come to dominate open-weight model development in a way that
few in the West anticipated.
<a href="https://www.technologyreview.com/2026/02/12/1132811/whats-next-for-chinese-open-source-ai/">Alibaba’s
Qwen family has overtaken Meta’s Llama in cumulative Hugging Face downloads</a>,
and by mid-2025 Qwen-derived models accounted for over 40% of new language model
uploads on the platform. <a href="https://arxiv.org/abs/2501.12948">DeepSeek R1</a>
briefly narrowed the frontier gap to
<a href="https://epoch.ai/data-insights/us-vs-china-eci">as little as four months</a>.
<a href="https://huggingface.co/zai-org/GLM-5">GLM-5</a>,
<a href="https://github.com/MoonshotAI/Kimi-K2.5">Kimi K2.5</a>, and
<a href="https://huggingface.co/MiniMaxAI/MiniMax-M2.5">MiniMax M2.5</a>, all
released as open weights under permissive licenses in early 2026, are
<a href="https://www.latent.space/p/ainews-zai-glm-5-new-sota-open-weights">frontier-adjacent
at a fraction of the inference cost</a>. Despite hardware constraints that leave
Chinese labs working around export controls on cutting-edge chips, they have
executed a remarkably effective strategy: flood the world with competitive
open-weight models and let the ecosystem do the rest.</p>

<p>Western open-source, by contrast, is thin.
AI2 ships <a href="https://arxiv.org/abs/2512.13961">OLMo3</a>,
Hugging Face has <a href="https://huggingface.co/blog/smollm3">SmolLM3</a>,
and Mistral has <a href="https://mistral.ai/">its lineup</a>.
Beyond that, the list of Western labs releasing serious open-weight
foundation models is short. This is a strange asymmetry: Western labs hold a
<a href="https://techblog.comsoc.org/2026/01/27/chinas-open-source-ai-models-to-capture-a-larger-share-of-2026-global-ai-market/">nearly
tenfold advantage in compute infrastructure</a> (U.S. hyperscaler capex alone
exceeded $400 billion in 2025, versus roughly $57 billion across
China’s major platforms), yet produce far fewer open-weight models. China
is not winning open-source because of a technical edge in hardware. They are
winning because almost nobody in the West is trying.</p>

<p>That matters because open-weight models are not just a convenience; they are
infrastructure. Researchers, startups, and institutions that cannot afford
per-token API pricing or cannot send sensitive data to a third-party endpoint
depend on models they can run locally. If the only competitive options come from
Chinese labs, that shapes who builds what, on whose stack, and on whose terms.</p>

<p><a href="https://www.arcee.ai">Arcee AI</a> is one of the Western labs taking
this challenge seriously, and the name of their model family gives away exactly
how: <strong>Trinity</strong> is a three-way collaboration.
Arcee AI led the model architecture and training.
<a href="https://www.primeintellect.ai">Prime Intellect</a> contributed their
distributed training infrastructure, a platform that lets researchers train large models
across decentralised GPU clusters without needing a hyperscaler budget.
<a href="https://datalogy.ai">Datalogy</a> curated the training data.
Together they released a full family: <strong>Trinity Nano</strong> for
edge and on-device deployment, <strong>Trinity Mini</strong> as the practical mid-range option, and
<strong>Trinity Large</strong> at the frontier scale, competitive with
<a href="https://www.arcee.ai/blog/trinity-large">GLM-4.5 base-to-base</a>.
All three are fully open-weight under a permissive license. We use Trinity Mini
for this study because its sparse MoE keeps inference cost low even when generating
thousands of rollouts per step, which is exactly what RLVR post-training demands.</p>

<p>The open-weight advantage is not just about what a model scores at release; it is about
what you can build on top of it. Targeted RLVR post-training can unlock domain-specific
capabilities that general benchmarks do not capture, and that is where open-source models
have real room to close the gap. The question we set out to answer is simple:
<strong>Can domain-specific RLVR post-training turn Trinity Mini into a meaningfully better
model for a real workload?</strong></p>

<p>The task we chose is biomedical relation extraction. At
<a href="https://loka.com">Loka</a>, healthcare and life sciences projects make
up a significant part of our client work, which is part of why this domain
appealed to us. Drug discovery, clinical decision support, and pharmacovigilance
all depend on extracting structured knowledge from unstructured text. PubMed
indexes over 36 million citations, and the relationships buried in those
abstracts (which drug inhibits which protein, which compound is a substrate for
which enzyme) are the raw material for computational pharmacology.
<a href="https://biocreative.bioinformatics.udel.edu/tasks/biocreative-vii/track-1/">DrugProt</a>
(BioCreative VII) is a benchmark for exactly this: 13 types of drug-protein
interactions, annotated on real PubMed abstracts.</p>

<p>The method is <strong>Reinforcement Learning from Verifiable Rewards
(RLVR)</strong>: rather than training a separate reward model, the environment
provides a ground-truth accuracy signal directly (correct label = 1, wrong = 0).
<a href="https://arxiv.org/abs/2402.03300">GRPO (Group Relative Policy
Optimization)</a> makes this tractable by estimating policy gradients from groups of sampled
completions, eliminating the critic network required by
<a href="https://arxiv.org/abs/1707.06347">PPO</a> and
substantially reducing memory overhead.
<a href="https://arxiv.org/abs/2501.12948">DeepSeek R1</a> then demonstrated
at scale that RLVR alone, with no supervised chain-of-thought, can teach models
to reason step by step. We apply the same idea to biomedical relation
classification.</p>

<p>Because GRPO generates many rollouts per training step (up to 1,024 completions
in our experiments), the dominant cost is inference, not gradient computation.
Full post-training a 26B parameter MoE under these conditions is
prohibitively expensive: every parameter update must be applied after each batch
of thousands of sampled completions, requiring enormous GPU memory and wall-clock
time. We therefore use
<a href="https://arxiv.org/abs/2106.09685">LoRA (Low-Rank Adaptation)</a>
, which inserts small trainable rank
decomposition matrices into the frozen base model, reducing trainable parameters
by several orders of magnitude while keeping the base weights intact.
Crucially, this is not a performance compromise:
<a href="https://thinkingmachines.ai/blog/lora/">LoRA Without Regret</a>
shows that LoRA fully matches full post-training in policy
gradient RLVR even at rank 1, because RLVR absorbs far less information per
update than supervised learning. We run the full pipeline on
<a href="https://www.primeintellect.ai">Prime Intellect</a>’s
training infrastructure.</p>

<p>We ran 12 experiments sweeping LoRA alpha, learning rate, batch size, generation
budget, temperature, and rollout count. The question is deliberately practical:
<strong>Which hyperparameters actually move the needle</strong> when you RLVR-tune
an open-weight sparse MoE for biomedical relation extraction?</p>

<h2 id="the-task--the-model">The task &amp; the model</h2>

<h3 id="drugprot-the-rl-environment">DrugProt: the RL environment</h3>

<p><a href="https://biocreative.bioinformatics.udel.edu/tasks/biocreative-vii/track-1/">DrugProt</a>
(BioCreative VII Track 1) is a corpus of PubMed abstracts annotated with 13 types
of drug-protein interactions: INHIBITOR, SUBSTRATE, INDIRECT-DOWNREGULATOR,
INDIRECT-UPREGULATOR, AGONIST, ANTAGONIST, ACTIVATOR, PRODUCT-OF, and five others.
Given an abstract and a specific (chemical, gene/protein) pair, the model must
classify the relationship.</p>

<p>We use the
<a href="https://app.primeintellect.ai/dashboard/environments/maziyar/openmed_drugprot"><code class="language-plaintext highlighter-rouge">maziyar/OpenMed_DrugProt@0.3.0</code></a>
environment, which frames DrugProt as a <strong>13-way multiple choice question</strong>:
each relation type maps to a letter A through M. The model sees the full PubMed abstract,
the highlighted entities, and must output its answer inside <code class="language-plaintext highlighter-rouge">\boxed{}</code> tags.</p>

<p>Here is a representative example from the dataset:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Example input

Abstract:
Na+/Ca2+ exchange inhibitors: a new class of calcium regulators. The Na(+)/Ca(2+) exchanger (NCX) is a bidirectional transporter that normally extrudes Ca(2+) from the cell (forward mode), but also brings Ca(2+) into the cell (reverse mode) under special conditions such as intracellular Na(+) accumulation or membrane depolarization. There are three mammalian NCX isoforms: NCX1 is widely expressed in the heart, kidney, brain, blood vessels, and so on; whereas the expression of NCX2 and NCX3 is limited mainly to the brain and skeletal muscle. […]

Drug/Chemical: Ca(2+)
Gene/Protein: Na(+)/Ca(2+) exchanger

Question:
What is the relation type between Ca(2+) and Na(+)/Ca(2+) exchanger?

Options:
A. INDIRECT-DOWNREGULATOR
B. INDIRECT-UPREGULATOR
C. DIRECT-REGULATOR
D. ACTIVATOR
E. INHIBITOR
F. AGONIST
G. AGONIST-ACTIVATOR
H. AGONIST-INHIBITOR
I. ANTAGONIST
J. PRODUCT-OF
K. SUBSTRATE
L. SUBSTRATE_PRODUCT-OF
M. PART-OF

Correct answer:
K - SUBSTRATE (Ca(2+) is transported by the NCX, making it a substrate of the exchanger)
</code></pre></div></div>

<h3 id="reward-structure">Reward structure</h3>

<p>The environment defines a composite reward with three components:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>reward = 0.70 * accuracy + 0.20 * reasoning + 0.10 * format
</code></pre></div></div>

<ul>
  <li><strong>Accuracy (weight: 0.70):</strong> Binary, 1.0 if the boxed letter
matches the gold label, 0.0 otherwise. This is the signal that matters.</li>
  <li><strong>Reasoning (weight: 0.20):</strong> Heuristic score composed of three
components: (1) length of the <code class="language-plaintext highlighter-rouge">&lt;think&gt;&lt;/think&gt;</code> block
(0.1–0.4 depending on whether it exceeds 100 / 200 / 400 characters),
(2) keyword density from a fixed list of 37 biomedical terms — agonist,
inhibit, substrate, kinase, transporter, etc. — capped at 0.3, and
(3) presence of the words “chemical” or “drug”
(+0.1) and “protein” or “gene” (+0.1).
Crucially, this reward is <strong>task-agnostic</strong>: it does not check
whether the reasoning is correct or relevant to the specific entity pair.
A model writing generic biomedical prose scores well regardless of whether
its analysis of <em>this</em> relationship is right. This explains why
reasoning reward improves similarly across all alpha values — it measures
format and vocabulary, not depth of analysis. It also makes the reasoning
component the noisiest of the three signals.</li>
  <li><strong>Format (weight: 0.10):</strong> Binary, 1.0 if the output contains
a valid <code class="language-plaintext highlighter-rouge">\boxed{A..M}</code>, 0.0 otherwise.</li>
</ul>

<p>This structure is intentional: accuracy dominates, but the shaping signals from
reasoning and format help bootstrap learning in early steps when the model has not
yet learned the task. A practical consequence: total reward can improve early via
format compliance and longer reasoning traces before accuracy actually moves.</p>

<h3 id="the-model-arcee-trinity-mini">The model: Arcee Trinity Mini</h3>

<p><a href="https://www.arxiv.org/pdf/2602.17004">Trinity Mini</a> is a sparse
mixture-of-experts (MoE) model from Arcee AI: 26B total parameters,
3B active per forward pass. Sparse MoE is a natural fit for RLVR post-training
because GRPO is dominated by forward passes. With
<code class="language-plaintext highlighter-rouge">batch_size=128</code> and <code class="language-plaintext highlighter-rouge">rollouts_per_example=8</code>, each training
step generates 1,024 sampled completions. A model that is cheap at inference time
but has high total capacity is exactly what you want.</p>

<p>We adapt Trinity Mini using
<a href="https://arxiv.org/abs/2106.09685">LoRA (Low-Rank Adaptation)</a>.
LoRA freezes the base model and learns two low-rank matrices <em>A</em> and
<em>B</em> per targeted layer (rank <em>r</em>), scaled by <em>α</em>.</p>

<p>Δ<em>W</em> = (<em>α</em>/<em>r</em>) · <em>B**A</em></p>

<p>The key hyperparameter we ablate is <strong>α</strong>: it controls how
much the LoRA updates influence the model’s outputs independently of the
number of trainable parameters. Higher α means stronger adaptation per
step; lower α is more conservative. Note that α acts as a multiplier
on the effective learning rate for the adapter layers; raising α and
raising the global LR have similar effects on training dynamics.</p>

<p><strong>Important Distinction:</strong> We sweep LoRA <em>alpha</em> (the
scaling factor), not LoRA <em>rank</em>. Alpha controls update magnitude without
changing the number of trainable parameters. This makes it a pure “how
aggressively do we adapt?” knob.</p>

<h2 id="experiments--results">Experiments &amp; results</h2>

<p>Our ablation strategy is sequential. We sweep one variable at a time, lock in the
best setting, and move to the next. This gives us a clear cause-and-effect story
at the cost of not exploring the full combinatorial space. For each comparison we
show learning curves (accuracy reward over training steps) with a 5-step moving
average for readability, plus snapshot bar charts at key steps.</p>

<p>One parameter we did not sweep is LoRA rank. Prime Intellect’s training
infrastructure fixes rank at <code class="language-plaintext highlighter-rouge">r=16</code>, applied across all projection
layers (<code class="language-plaintext highlighter-rouge">q_proj</code>, <code class="language-plaintext highlighter-rouge">k_proj</code>, <code class="language-plaintext highlighter-rouge">v_proj</code>,
<code class="language-plaintext highlighter-rouge">o_proj</code>, <code class="language-plaintext highlighter-rouge">gate_proj</code>, <code class="language-plaintext highlighter-rouge">up_proj</code>,
<code class="language-plaintext highlighter-rouge">down_proj</code>, and expert layers). We are comfortable leaving rank
fixed because
<a href="https://thinkingmachines.ai/blog/lora/">LoRA Without Regret</a>
establishes that policy gradient algorithms absorb roughly
one bit of information per episode independent of model size, meaning a
rank-1 LoRA (3M parameters for an 8B model) vastly exceeds the
capacity needed for typical RL training sets. Rank is therefore not the binding
constraint in our setting. The same work also demonstrates that applying LoRA to
all layers (attention <em>and</em> MLPs) substantially outperforms
attention-only LoRA — a finding consistent with our all-layer setup.
On Trinity Mini this is especially relevant: the model’s G1 gated
attention uses a dedicated <code class="language-plaintext highlighter-rouge">gate_proj</code> weight matrix in every
attention layer that is both a LoRA target and architecturally load-bearing
(it controls attention output magnitudes via a learned sigmoid gate), so
leaving it frozen would mean skipping a significant set of parameters.</p>

<p>All 12 experiments (click to expand)</p>

<p>Unless noted otherwise, defaults are:
<code class="language-plaintext highlighter-rouge">batch_size=128</code>, <code class="language-plaintext highlighter-rouge">rollouts_per_example=8</code>,
<code class="language-plaintext highlighter-rouge">temperature=0.7</code>, <code class="language-plaintext highlighter-rouge">max_tokens=512</code>.</p>

<table>
  <thead>
    <tr>
      <th>Run</th>
      <th>α</th>
      <th>LR</th>
      <th>Batch</th>
      <th>Rollouts</th>
      <th>Max tokens</th>
      <th>Temp</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w1-alpha16-baseline</code></td>
      <td>16</td>
      <td>1e-6</td>
      <td>128</td>
      <td>8</td>
      <td>512</td>
      <td>0.7</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w1-alpha32-baseline</code></td>
      <td>32</td>
      <td>1e-6</td>
      <td>128</td>
      <td>8</td>
      <td>512</td>
      <td>0.7</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w1-alpha64-baseline</code></td>
      <td>64</td>
      <td>1e-6</td>
      <td>128</td>
      <td>8</td>
      <td>512</td>
      <td>0.7</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w1-alpha128-baseline</code></td>
      <td>128</td>
      <td>1e-6</td>
      <td>128</td>
      <td>8</td>
      <td>512</td>
      <td>0.7</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w1-alpha64-lr1e-5</code></td>
      <td>64</td>
      <td>1e-5</td>
      <td>128</td>
      <td>8</td>
      <td>512</td>
      <td>0.7</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w1-alpha64-lr5e-6</code></td>
      <td>64</td>
      <td>5e-6</td>
      <td>128</td>
      <td>8</td>
      <td>512</td>
      <td>0.7</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w1-alpha64-lr3e-6</code></td>
      <td>64</td>
      <td>3e-6</td>
      <td>128</td>
      <td>8</td>
      <td>512</td>
      <td>0.7</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w1-alpha64-lr3e-6-b64-r16</code></td>
      <td>64</td>
      <td>3e-6</td>
      <td>64</td>
      <td>16</td>
      <td>512</td>
      <td>0.7</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w1-alpha64-lr3e-6-1024</code></td>
      <td>64</td>
      <td>3e-6</td>
      <td>128</td>
      <td>8</td>
      <td>1024</td>
      <td>0.7</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w1-alpha64-lr3e-6-2048</code></td>
      <td>64</td>
      <td>3e-6</td>
      <td>128</td>
      <td>8</td>
      <td>2048</td>
      <td>0.7</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w2-2048-t0.15</code></td>
      <td>64</td>
      <td>3e-6</td>
      <td>128</td>
      <td>8</td>
      <td>2048</td>
      <td>0.15</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w2-2048-r4</code></td>
      <td>64</td>
      <td>3e-6</td>
      <td>128</td>
      <td>4</td>
      <td>2048</td>
      <td>0.7</td>
    </tr>
  </tbody>
</table>

<h3 id="1-lora-alpha-ablation-16--32--64--128">1) LoRA alpha ablation (16 / 32 / 64 / 128)</h3>

<p>We started with the most fundamental LoRA knob: the alpha scaling factor.
Keeping everything else fixed (LR 1e-6, batch 128, rollouts 8,
max_tokens 512), we trained four runs at α=16, α=32, α=64,
and α=128. The general picture is clear: up to α=64, higher alpha
produces both faster learning and higher accuracy. α=64 is the best
overall setting, reaching ~0.75 accuracy reward at step 100 versus ~0.70
for α=32 and ~0.41 for α=16.</p>

<p><img src="/assets/images/drug-think/figure1.png" alt="Figure 1 - LoRA alpha sweep" /></p>

<p>Figure 1. LoRA alpha sweep (steps 1–100). Accuracy reward vs training
step for α=16, 32, 64, and 128, all at LR 1e-6. α=128
is terminated at step 70 due to eval degradation.</p>

<p>The exception is α=128. On the training metric it looks competitive
, even outperforming α=64 at matching steps, but the
held-out eval tells the opposite story. Figure 2 makes this impossible
to miss: α=128 started with the highest eval score of any run and ended
with the lowest, dropping steadily from step 0. The model was terminated
at step 70. Training reward is healthy because aggressive updates adapt
quickly to each batch; the eval exposes that generalization is collapsing at
the same rate.</p>

<p><img src="/assets/images/drug-think/figure2.png" alt="Figure 2 - Held-out eval score by LoRA alpha" /></p>

<p>Figure 2. Held-out eval score (<code class="language-plaintext highlighter-rouge">avg@2</code>) over training steps for
each LoRA alpha. α=64 climbs steadily; α=128 declines from the
very first checkpoint and is terminated at step 70. α=16 and
α=32 stay roughly flat.</p>

<p><img src="/assets/images/drug-think/figure3.png" alt="Figure 3 - Reasoning reward by LoRA alpha" /></p>

<p>Figure 3. Reasoning reward by LoRA alpha (steps 1–100). Unlike
accuracy, reasoning reward improves similarly across all alphas. The
model learns to produce structured thinking traces regardless of update
magnitude.</p>

<p>Reasoning reward tells a different story from accuracy. While α=64
clearly outperforms α=16 on accuracy, all four alphas produce similarly
improving reasoning traces over time. The model learns to write structured
pharmacological analysis regardless of how aggressively the LoRA layers are
being updated. This suggests that reasoning quality, as measured by our
heuristic, is more about training duration than update magnitude, a
contrast worth keeping in mind when interpreting the max tokens results below.</p>

<p>Alpha is not a dial where higher is always better. The LoRA scaling factor is
<code class="language-plaintext highlighter-rouge">α / rank</code>, so a higher alpha directly amplifies the
magnitude of every adapter update — effectively inflating the learning rate for
the LoRA layers independently of the global LR. The consequences are threefold.
First, the adapted weights receive disproportionately large updates, producing
loss spikes and erratic training dynamics. Second,
the model overfits aggressively to each batch: training accuracy climbs fast,
but the pretrained feature space gets warped at the same rate, eroding the
general capabilities inherited from pretraining. Third, and most visible in
Figure 3, generalization collapses on held-out data while the training
signal stays deceptively healthy.</p>

<p>The key practical insight is that alpha and learning rate are not independent
knobs: raising alpha has the same effect as raising the LR for the adapter
layers. If instability appears, lowering either will help. α=64 sat in
the stable, productive middle of our sweep, and we locked it in for all
subsequent experiments.</p>

<h3 id="2-learning-rate-sweep-at-α64-1e-5--1e-6--3e-6--5e-6">2) Learning rate sweep at α=64 (1e-5 / 1e-6 / 3e-6 / 5e-6)</h3>

<p>With alpha fixed at 64, we swept the learning rate across four values:
1e-6 (our alpha baseline), 3e-6, 5e-6, and the aggressive 1e-5. This
is where the story gets interesting.</p>

<p>The three moderate learning rates (1e-6, 3e-6, 5e-6) all produced healthy
learning curves with different speed/stability tradeoffs. 5e-6 learned fastest
early on. 3e-6 was steady and reliable. 1e-6 was conservative but safe.</p>

<p><strong>1e-5, however, was catastrophic.</strong> The run started promisingly; it
actually climbed faster than the others in the first 20 steps. But by step 50
the reward began oscillating wildly, and by step 77 accuracy collapsed to
zero. The run was terminated at step 82 with no recovery in sight. This is
not a gradual degradation; it is a cliff. The model essentially forgot how to
produce valid outputs.</p>

<p><img src="/assets/images/drug-think/figure4.png" alt="Figure 4 - Learning rate sweep at alpha 64" /></p>

<p>Figure 4. Learning rate sweep at α=64 (steps 1–100). The 1e-5 run
(red) climbs early but collapses catastrophically around step 50–77. The
remaining three learning rates show the classic speed-vs-stability tradeoff.</p>

<p><img src="/assets/images/drug-think/figure5.png" alt="Figure 5 - Accuracy reward at step 82 by learning rate" /></p>

<p>Figure 5. Accuracy reward at step 82 vs learning rate (log scale). The cliff
between 5e-6 and 1e-5 is dramatic: a 2× increase in LR produces
a collapse from ~0.83 to near zero.</p>

<p><strong>Why 1e-5 collapses and what LoRA Without Regret says about it:</strong>
<a href="https://thinkingmachines.ai/blog/lora/">LoRA Without Regret</a>
finds that the optimal LoRA learning rate is roughly
<strong>10× the optimal full post-training LR</strong> across a wide
range of models and tasks (15× for short runs of ~100 steps). On first
reading, this suggests we should be running at 1e-5 or higher. So why did
1e-5 destroy our model?</p>

<p>The key is the interaction between LR and α. LoRA’s weight update
is ΔW = (α/r)·BA. At α=64, r=16 the
scaling factor is 4; at the α=32 used in LoRA Without Regret it is 2.
Our effective per-step update magnitude at LR 1e-5 is therefore
equivalent to running at LR 2e-5 under their parameterization,
already at the edge of their reported stability range. Additionally, LoRA
Without Regret’s experiments use dense Llama-class architectures; sparse
MoE models route tokens through only a subset of experts per forward pass,
which can amplify gradient variance and narrow the stable LR window further.
The collapse at 1e-5 is therefore consistent with the theory once the
α-scaling and MoE architecture are both accounted for: we were above
the stability boundary for <em>this specific combination</em> of adapter
scaling and model class.</p>

<p>Two Trinity Mini-specific factors are worth flagging. First, Trinity Mini
was pre-trained with <a href="https://arxiv.org/abs/2203.03466">μP</a> input
scaling (embedding outputs multiplied by √2048 ≈ 45 before
each transformer layer), a different pre-training regime from the standard
architectures used in LoRA
Without Regret’s experiments; their empirical LR bounds were not
established under equivalent conditions. Second, MoE models face a
documented instability problem under GRPO specifically: top-k discrete
routing causes the set of activated experts to drift between inference and
training, <a href="https://arxiv.org/abs/2510.23027">inflating importance-sampling
variance</a> in a way that dense models do not experience.
These two factors make empirical LR validation more important here than on
a standard dense model.</p>

<p>The practical takeaway: when porting the LoRA Without Regret 10× rule
to a new setup, adjust for your actual α/r ratio and verify stability
empirically at the high end before committing to a full run.</p>

<p>At step 82 (the last step before 1e-5 data disappears): 5e-6 leads with
0.83 accuracy, followed by 3e-6 at 0.72, 1e-6 at 0.70, and 1e-5 at 0.00.
However, 5e-6 showed signs of instability in later steps. We chose
<strong>3e-6</strong> as the safest default that still converges quickly.</p>

<h3 id="3-batch-size-1288-vs-6416">3) Batch size: 128×8 vs 64×16</h3>

<p>Next we tested whether the same total completions per step (1,024) should come
from more examples with fewer rollouts or fewer examples with more rollouts.
We compared our default (<code class="language-plaintext highlighter-rouge">batch=128, rollouts=8</code>) against
(<code class="language-plaintext highlighter-rouge">batch=64, rollouts=16</code>), both at α=64, LR 3e-6.</p>

<p><img src="/assets/images/drug-think/figure6.png" alt="Figure 6 - Batch size comparison" /></p>

<p>Figure 6. Batch size comparison (steps 1–100). Accuracy reward for
128×8 (more examples, fewer rollouts) vs 64×16 (fewer examples,
more rollouts per example).</p>

<p>The two configurations performed comparably. At step 100, accuracy was
~0.74 for 128×8 and ~0.75 for 64×16. The key difference was cost:
64×16 generated notably longer completions on average (~222 vs ~167
tokens), meaning more inference compute per step for similar accuracy. We kept
<strong>128×8</strong> as the default.</p>

<h3 id="4-max-generation-tokens-512--1024--2048">4) Max generation tokens (512 / 1024 / 2048)</h3>

<p>With α=64, LR 3e-6, and batch 128×8 locked in, we swept the
maximum generation length. This knob changes both compute cost and the model’s
behavior, as longer caps allow more elaborate reasoning traces.</p>

<p><img src="/assets/images/drug-think/figure7.png" alt="Figure 7 - Max tokens sweep" /></p>

<p>Figure 7. Max tokens sweep (steps 1–100). Accuracy reward for 512, 1024,
and 2048 max tokens, all at α=64, LR 3e-6, batch 128×8.</p>

<p><img src="/assets/images/drug-think/figure8.png" alt="Figure 8 - Composite reward at step 100 by max tokens" /></p>

<p>Figure 8. Mean composite reward at step 100 by max tokens. 2048 achieves the
highest overall reward, driven by both accuracy and richer reasoning traces.</p>

<p><img src="/assets/images/drug-think/figure9.png" alt="Figure 9 - Reasoning reward by max tokens" /></p>

<p>Figure 9. Reasoning reward by max tokens (steps 1–100). The gap is
structural and visible from step 1: 2048 tokens starts at ~0.87,
1024 at ~0.81, and 512 at ~0.62, before any learning has occurred.
The reasoning heuristic rewards think blocks of 400+ characters, which a
512-token budget cannot reliably accommodate alongside the answer.</p>

<p>The 2048 configuration stood out as the strongest overall performer, and
Figure 9 makes the mechanism explicit. This is not a learned behaviour; it is a structural constraint. With 512 max tokens, the model must
fit both its reasoning trace and its boxed answer into a tight budget; reliably
hitting the 400+ character threshold for the reasoning reward becomes
structurally difficult. With 2048 tokens, the model has room to reason from
step 1, and that head-start compounds throughout training. Since reasoning
carries a 0.20 weight in the composite reward, the advantage is immediate and
persistent.</p>

<p>The cost, however, is real: at step 100, mean completion length is ~167
tokens at 512, ~334 at 1024, and ~605 at 2048. That is a 3.6× increase in
inference tokens per completion. We treat <strong>2048</strong> as the quality
setting for the final ablations.</p>

<h3 id="5-final-ablations-temperature-and-rollout-budget">5) Final ablations: temperature and rollout budget</h3>

<p>Our final two experiments started from the best configuration found so far
(α=64, LR 3e-6, batch 128, max_tokens 2048) and probed
two remaining knobs:</p>

<ul>
  <li><strong>Temperature 0.15:</strong> Does lower sampling entropy improve
accuracy by concentrating probability on the model’s best guesses?</li>
  <li><strong>Rollout 4:</strong> Can we halve the rollout budget (and thus
inference cost) per step without losing accuracy?</li>
</ul>

<p><img src="/assets/images/drug-think/figure10.png" alt="Figure 10 - Final ablations" /></p>

<p>Figure 10. Final ablations (steps 1–100). Accuracy reward for the 2048
baseline, the low-temperature variant (t=0.15), and the reduced-rollout variant
(r=4).</p>

<p>The rollout=4 variant matched or exceeded the baseline’s accuracy
throughout training, suggesting that for this task, 4 rollouts per example
provide sufficient signal for GRPO updates. This is significant because it
halves inference cost per training step.</p>

<p>The low-temperature variant (t=0.15) told a more nuanced story. It started
with very high accuracy in early steps (the model’s existing knowledge,
sampled deterministically, was already decent). But over the course of training,
the reduced exploration appeared to limit improvement. Lower temperature means
less diversity in rollouts, which means less informative gradient signal for GRPO.</p>

<h2 id="key-findings--best-configuration">Key findings &amp; best configuration</h2>

<p>After 12 experiments, here is what we learned:</p>

<p><strong>1. LoRA alpha is the single most important knob.</strong>
The difference between α=16 and α=64 was larger than any other
hyperparameter change we tested. This is not about adding more parameters (rank
was fixed); it is purely about update magnitude. If you are running LoRA
with RLVR, tune alpha before anything else.</p>

<p><strong>2. Learning rate 1e-5 is a hard boundary for LoRA + RLVR.</strong>
The collapse is not gradual. The model goes from learning to producing garbage
within ~30 steps. This is a direct consequence of LoRA’s alpha-scaled
updates interacting with GRPO’s policy gradient: when the effective step
size is too large, the policy moves so far from the reference that the KL penalty
cannot recover. Anyone running RLVR with LoRA adapters should treat 1e-5 as a
red line, not a starting point.</p>

<p><strong>3. More thinking room helps, but you pay for it.</strong>
Going from 512 to 2048 max tokens improved composite reward meaningfully. The
model produced richer, more pharmacologically grounded reasoning. But completion
length tripled, and the accuracy gains themselves were noisy. The right framing is:
max_tokens is a cost-quality dial, not a free lunch.</p>

<p><strong>4. You can cut rollouts without losing accuracy.</strong>
The rollout=4 experiment was our most practically useful finding. Halving rollouts
per example halves inference cost per training step, and on this task the model
maintained strong accuracy. This suggests that for well-structured classification
tasks with clear reward signals, you do not need 8 rollouts to get good GRPO
gradients.</p>

<p><strong>5. Low temperature hurts RLVR exploration.</strong>
Temperature 0.15 gave a strong start (the model’s existing knowledge was
sampled more deterministically) but underperformed over the full 100 steps. RLVR
needs diversity in rollouts to compute meaningful advantages. Squeezing exploration
too early is counterproductive.</p>

<p><strong>Best configuration:</strong>
α=64, LR 3e-6, batch 128, rollouts 8 (or 4 for cost
savings), max_tokens 2048, temperature 0.7.
This combination produced the highest overall reward and maintained stable
learning throughout training.</p>

<h3 id="the-output-contrast-best-model-vs-catastrophic-1e-5">The output contrast: best model vs catastrophic 1e-5</h3>

<p>To make the impact concrete, here is a side-by-side comparison of model outputs
on the same DrugProt example. The best configuration produces structured
pharmacological reasoning; the collapsed 1e-5 model produces incoherent output.</p>

<p>Best model (α=64, lr=3e-6, 2048 tokens)</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;think&gt;
The user is asking about the relation between “Ser” (serine, an amino acid) and
“GXSXG motif” (a conserved sequence motif in proteins). The options are
various types of interactions.

Key points from the biomedical text: Calnuc is a serine protease, and the text
mentions “Ser-378 of GXSXG motif”. This indicates that serine (Ser) is part
of the GXSXG motif, specifically at position 378. The GXSXG motif is a catalytic
motif in serine proteases, where serine acts as the catalytic residue. The text
states that calnuc has two Zn²+-binding sites and is inactive as a peptidase.
Zn²+ binding negatively regulates serine protease activity by causing a
conformational change. G protein α subunit also inhibits enzymatic activity.

Analyzing the options: A–I are about regulation (inhibition, activation, etc.),
but the relationship here is that Ser is part of the motif, not regulating it.
J–M are about the chemical’s role in relation to an enzyme’s process.
M is PART-OF, which fits because Ser is a component of the GXSXG motif. Other
options don’t fit: J–L are about the chemical being a product, substrate,
or both; A–I are about regulation. Ser here is a residue in a protein motif,
not a product, substrate, or external regulator.

Final answer: M.
&lt;/think&gt;

The text explicitly states that Ser-378 is part of the GXSXG motif in calnuc.
Serine is a component residue within the motif, making it a structural constituent
rather than an external regulator or product/substrate.

\boxed{M}
</code></pre></div></div>

<p>Collapsed model (lr=1e-5, step ~80)</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;think&gt;
the the the the the the the the the the the the the the the
the the the the the the the the the the the the the the the
the the the the the the the the the the the the the the the
&lt;/think&gt;

\boxed{A}
</code></pre></div></div>

<h2 id="next-steps">Next steps</h2>

<ul>
  <li><strong>Longer training runs.</strong> Our best configuration (α=64,
LR 3e-6, 2048 tokens) was capped at ~100 steps and its eval curve had
not flattened. Running it for 500+ steps is the clearest path to a stronger
model, though diminishing returns and eval divergence both remain risks.</li>
  <li><strong>Evaluation on the official DrugProt test set.</strong> Our held-out
metric (<code class="language-plaintext highlighter-rouge">avg@2</code>) is computed on the training split during RLVR. A
proper evaluation on the official test set would tell us whether the gains
reflect genuine generalization or a more narrow adaptation to the training
distribution.</li>
  <li><strong>Trying different base models.</strong> Trinity Mini is one choice.
Running the same ablation protocol on other open-weight models would reveal
whether the alpha and LR sensitivities we found are MoE-specific or general
properties of LoRA under RLVR.</li>
  <li><strong>Better reward signals.</strong> The reasoning reward is heuristic
(keyword matching and length thresholds). Replacing it with a learned reward
model or NLI-based evaluation could change the learning dynamics substantially,
especially for the reasoning component which was the noisiest signal.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>The recipe works. α=64, LR 3e-6, 2048 max tokens: a 3B-active-parameter
open-weight MoE that learns to reason about drug-protein interactions from reward
signals alone, no labeled chain-of-thought required. The leverage is almost
entirely in two numbers; everything else is secondary.</p>

<p>If you want to use the fine-tuned model, you do not need to re-run the training.
We have published the adapter weights (<a href="https://huggingface.co/lokahq/Trinity-Mini-DrugProt-Think"><strong>lokahq/Trinity-Mini-DrugProt-Think</strong></a>) and written a step-by-step deployment guide
using the <strong>AWS SageMaker SDK v3</strong>, covering how to serve the merged
model as a real-time endpoint:
<a href="https://medium.com/loka-engineering/deploying-trinity-mini-drugprot-think-on-amazon-sagemaker-ai-9e1c1c430ce9">deployment guide</a>.
SageMaker’s managed inference handles scaling and hardware allocation;
the guide walks through container selection, endpoint configuration, and a
sample inference call against the DrugProt relation types.</p>

<p>We have also started running the same RLVR+LoRA recipe on
<strong>Trinity Nano</strong> (6B parameters), across both medical and
general reasoning tasks. Nano is small enough to serve cheaply on a single GPU
instance, which matters for inference in production use cases. We expect the same
hyperparameter sensitivities to hold; we will publish a follow-up when we have
results worth sharing.</p>

<h2 id="acknowledgements">Acknowledgements</h2>

<ul>
  <li><strong>Model:</strong> <a href="https://www.arcee.ai">Arcee AI</a> (with <a href="https://www.primeintellect.ai">Prime Intellect</a> and <a href="https://datalogy.ai">Datalogy</a>) for releasing the Trinity family.</li>
  <li><strong>Training:</strong> <a href="https://www.primeintellect.ai">Prime Intellect</a> for hosted training infrastructure.</li>
  <li><strong>Environment:</strong> <a href="https://huggingface.co/datasets/OpenMed/drugprot-parquet">OpenMed</a> for DrugProt dataset packaging.</li>
  <li><strong>Deployment:</strong> <a href="https://aws.amazon.com/">AWS</a> for deployment and hosting.</li>
</ul>

<h2 id="citation">Citation</h2>

<p>If you find this work useful, please cite:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@misc{jakimovski2026drugprotrl,
  title        = {Post-Training an Open MoE Model to Extract Drug-Protein Relations: Trinity-Mini-DrugProt-Think},
  author       = {Jakimovski, Bojan and Kalinovski, Petar},
  year         = {2026},
  month        = feb,
  howpublished = {Blog post},
  url          = {https://github.com/LokaHQ/Trinity-Mini-DrugProt-Think}
}
</code></pre></div></div>

<h2 id="further-reading">Further reading</h2>

<ol>
  <li>MIT Technology Review (2026).
<em>What’s next for Chinese open-source AI.</em>
Reports Qwen overtaking Llama in Hugging Face downloads and
Qwen-derived models reaching 40%+ of new language model uploads.
<a href="https://www.technologyreview.com/2026/02/12/1132811/whats-next-for-chinese-open-source-ai/">technologyreview.com</a></li>
  <li>Epoch AI (2026).
<em>Chinese AI models have lagged the US frontier by 7 months on average since 2023.</em>
Analysis of the Epoch Capabilities Index showing the US-China frontier gap
ranged from 4 to 14 months.
<a href="https://epoch.ai/data-insights/us-vs-china-eci">epoch.ai</a></li>
  <li>IEEE ComSoc Technology Blog (2026).
<em>China’s open source AI models to capture a larger share of 2026 global AI market.</em>
Cites Goldman Sachs and UBS data on the ~10× gap in compute
infrastructure investment between U.S. hyperscalers ($400B+) and
Chinese platforms (~$57B).
<a href="https://techblog.comsoc.org/2026/01/27/chinas-open-source-ai-models-to-capture-a-larger-share-of-2026-global-ai-market/">techblog.comsoc.org</a></li>
  <li>Latent Space (2026).
<em>[AINews] Z.ai GLM-5: New SOTA Open Weights LLM.</em>
Covers the February 2026 releases of GLM-5 (744B) and MiniMax M2.5 (230B).
<a href="https://www.latent.space/p/ainews-zai-glm-5-new-sota-open-weights">latent.space</a></li>
  <li>Z.ai (2026).
<em>GLM-5.</em>
Official open-weights model card.
<a href="https://huggingface.co/zai-org/GLM-5">huggingface.co/zai-org/GLM-5</a></li>
  <li>MoonshotAI (2026).
<em>Kimi K2.5.</em>
Official model repository and technical report.
<a href="https://github.com/MoonshotAI/Kimi-K2.5">github.com/MoonshotAI/Kimi-K2.5</a></li>
  <li>MiniMax (2026).
<em>MiniMax-M2.5.</em>
Official open-weights model card.
<a href="https://huggingface.co/MiniMaxAI/MiniMax-M2.5">huggingface.co/MiniMaxAI/MiniMax-M2.5</a></li>
  <li>MiniMax (2026).
<em>MiniMax M2.5: The official release announcement.</em>
<a href="https://www.minimaxi.com/news/minimax-m25">minimaxi.com</a></li>
  <li>Schulman, J. et al. (2017).
<em>Proximal Policy Optimization Algorithms.</em>
Introduces PPO.
<a href="https://arxiv.org/abs/1707.06347">arxiv.org/abs/1707.06347</a></li>
  <li>Schulman, J. (2025).
<em>LoRA Without Regret.</em>
Thinking Machines Lab: Connectionism.
DOI: 10.64434/tml.20250929.
Establishes that LoRA fully matches full post-training in policy gradient RL
even at rank 1. Derives an empirical LR scaling rule (optimal LoRA LR
≈ 10× full post-training LR), shows attention-only LoRA substantially
underperforms all-layer LoRA, and characterizes the α/r parameterization
invariance.
<a href="https://thinkingmachines.ai/blog/lora/">thinkingmachines.ai/blog/lora</a></li>
  <li>Hu, E. J. et al. (2022).
<em>LoRA: Low-Rank Adaptation of Large Language Models.</em>
International Conference on Learning Representations (ICLR).
Introduces low-rank decomposition adapters that reduce trainable parameters
by up to 10,000× compared to full post-training, with no inference
latency overhead.
<a href="https://arxiv.org/abs/2106.09685">arxiv.org/abs/2106.09685</a></li>
  <li>Shao, Z. et al. (2024).
<em>DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open
Language Models.</em>
Introduces GRPO (Group Relative Policy Optimization), which estimates policy
gradient baselines from groups of sampled completions, eliminating the critic
network required by <a href="https://arxiv.org/abs/1707.06347">PPO</a> and
substantially reducing memory cost.
<a href="https://arxiv.org/abs/2402.03300">arxiv.org/abs/2402.03300</a></li>
  <li>DeepSeek-AI (2025).
<em>DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via
Reinforcement Learning.</em>
Demonstrates that RLVR alone — with no supervised chain-of-thought
data — can teach models to reason step by step, achieving
AIME 2024 accuracy of 71.0% (86.7% with majority voting), matching
OpenAI o1.
<a href="https://arxiv.org/abs/2501.12948">arxiv.org/abs/2501.12948</a></li>
  <li>Arcee AI (2025).
<em>The Trinity Manifesto.</em>
Blog post introducing Trinity Mini and Trinity Nano, including benchmark
comparisons against Qwen3-30B-A3B Thinking, OLMo3-32B, and Magistral Small.
<a href="https://www.arcee.ai/blog/the-trinity-manifesto">arcee.ai/blog/the-trinity-manifesto</a></li>
  <li>Arcee AI (2025).
<em>Trinity Large.</em>
Blog post presenting Trinity Large benchmark comparisons against
GLM-4.5, GLM-4.7, DeepSeek-V3, Llama 4 Maverick, and MiniMax M2.1.
<a href="https://www.arcee.ai/blog/trinity-large">arcee.ai/blog/trinity-large</a></li>
  <li>Team OLMo (2025).
<em>OLMo3.</em>
Allen Institute for AI.
A fully open foundation model family (weights, training data, code) released
by AI2 as a contribution to transparent Western open-source AI research.
<a href="https://arxiv.org/abs/2512.13961">arxiv.org/abs/2512.13961</a></li>
  <li>Mistral AI (2025).
<em>Magistral.</em>
Mistral AI.
Mistral’s first reasoning model family, trained with RL on top of Mistral
Medium 3. Magistral Small (24B, Apache 2.0) is open-weight.
<a href="https://arxiv.org/abs/2506.10910">arxiv.org/abs/2506.10910</a></li>
  <li>Arcee AI (2026).
<em>Arcee Trinity Large Technical Report.</em>
Describes the Trinity family of sparse mixture-of-experts models, including
Trinity Mini (26B total / 3B active) used in this study.
<a href="https://www.arxiv.org/pdf/2602.17004">arxiv.org/pdf/2602.17004</a></li>
  <li>maziyar/OpenMed_DrugProt (2025).
<em>DrugProt RL environment for Prime Intellect.</em>
The verifiers environment used for all experiments in this study.
<a href="https://app.primeintellect.ai/dashboard/environments/maziyar/openmed_drugprot">app.primeintellect.ai</a></li>
  <li>Miranda, A. et al. (2021).
<em>Overview of DrugProt BioCreative VII Track 1: Advanced Biomedical Named
Entity Recognition and Relation Extraction.</em>
BioCreative VII Workshop. Introduces the DrugProt corpus of PubMed abstracts
annotated with 13 drug-protein relation types.</li>
  <li>OpenMed (2025).
<em>drugprot-parquet dataset.</em>
Parquet version of DrugProt used by the environment.
<a href="https://huggingface.co/datasets/OpenMed/drugprot-parquet">huggingface.co/datasets/OpenMed/drugprot-parquet</a></li>
  <li>Qwen Team (2025).
<em>Qwen3 Technical Report.</em>
Alibaba Cloud. Dense and MoE models from 0.6B to 235B parameters, with
unified thinking/non-thinking modes. Qwen3-30B-A3B is the MoE variant
used for comparison in this study.
<a href="https://arxiv.org/abs/2505.09388">arxiv.org/abs/2505.09388</a></li>
  <li>Zhang, D. et al. (2025).
<em>Towards Stable and Effective Reinforcement Learning for Mixture-of-Experts.</em>
Identifies top-k routing drift between inference and training as the root
cause of MoE instability under GRPO, and proposes RSPO to address it via
router-aware importance sampling.
<a href="https://arxiv.org/abs/2510.23027">arxiv.org/abs/2510.23027</a></li>
  <li>Yang, G. et al. (2022).
<em>Tensor Programs V: Tuning Large Neural Networks via Zero-Shot
Hyperparameter Transfer.</em>
NeurIPS 2022. Introduces Maximal Update Parametrization (μP), enabling
hyperparameter transfer across model widths during pre-training.
<a href="https://arxiv.org/abs/2203.03466">arxiv.org/abs/2203.03466</a></li>
</ol>]]></content><author><name>shekswess</name></author><category term="AI" /><category term="LLM" /><category term="RLVR" /><category term="GRPO" /><category term="LoRA" /><category term="Trinity Mini" /><category term="DrugProt" /><category term="Open Weight Models" /><category term="SageMaker" /><summary type="html"><![CDATA[A practical ablation study: GRPO-style reinforcement learning with LoRA on Arcee Trinity Mini for DrugProt drug-protein relation extraction.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://raw.githubusercontent.com/LokaHQ/Trinity-Mini-DrugProt-Think/main/assets/logo.png" /><media:content medium="image" url="https://raw.githubusercontent.com/LokaHQ/Trinity-Mini-DrugProt-Think/main/assets/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>