<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: JC</title>
    <description>The latest articles on DEV Community by JC (@jac-76).</description>
    <link>https://dev.to/jac-76</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4108034%2Fe1209f76-f9cc-41d1-a089-4cb32af9e673.png</url>
      <title>DEV Community: JC</title>
      <link>https://dev.to/jac-76</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jac-76"/>
    <language>en</language>
    <item>
      <title>Running Whisper + LLMs on an AMD NPU under Linux</title>
      <dc:creator>JC</dc:creator>
      <pubDate>Thu, 03 Sep 2026 12:40:54 +0000</pubDate>
      <link>https://dev.to/jac-76/running-whisper-llms-on-an-amd-npu-under-linux-2o1h</link>
      <guid>https://dev.to/jac-76/running-whisper-llms-on-an-amd-npu-under-linux-2o1h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — On a MSI Stealth A16 AI+ (Ryzen AI 9 365, XDNA2 NPU) running Arch,&lt;br&gt;
I got OpenAI's &lt;code&gt;whisper-large-v3-turbo&lt;/code&gt; transcribing on the &lt;strong&gt;NPU&lt;/strong&gt; — not the&lt;br&gt;
CPU, not the GPU — at &lt;strong&gt;RTF ≈ 0.18&lt;/strong&gt; (a 30 s clip in ~5.2 s) for roughly a&lt;br&gt;
&lt;strong&gt;tenth of the energy&lt;/strong&gt; the same job costs on the CPU, plus an LLM answering&lt;br&gt;
on the same NPU through an OpenAI-compatible API. The&lt;br&gt;
whole path is local and offline. This is the write-up of the driver stack,&lt;br&gt;
the one real gotcha (memlock), and the runtime that made it a 20-minute job&lt;br&gt;
instead of a weekend.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why this is worth writing down
&lt;/h2&gt;

&lt;p&gt;AMD's "Ryzen AI" NPU (the XDNA / XDNA2 block in Phoenix / Hawk Point / Strix&lt;br&gt;
Point laptops) is marketed almost entirely around Windows: the Ryzen AI SDK,&lt;br&gt;
the ONNX Runtime &lt;strong&gt;VitisAI&lt;/strong&gt; execution provider, Lemonade, and the demos all&lt;br&gt;
assume you're on Windows with the official stack. On Linux the picture in early&lt;br&gt;
2026 is better than most people think — the NPU driver has been &lt;strong&gt;in the&lt;br&gt;
mainline kernel&lt;/strong&gt; as &lt;code&gt;amdxdna&lt;/code&gt; since 6.14 — but the "load a real model and run&lt;br&gt;
it" story still isn't well documented.&lt;/p&gt;

&lt;p&gt;Here's what actually worked, end to end.&lt;/p&gt;
&lt;h3&gt;
  
  
  The hardware
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Laptop&lt;/td&gt;
&lt;td&gt;MSI Stealth A16 AI+ A3HVGG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APU&lt;/td&gt;
&lt;td&gt;AMD Ryzen AI 9 365 (Strix Point)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NPU&lt;/td&gt;
&lt;td&gt;XDNA2, 8 columns, exposed as &lt;code&gt;/dev/accel/accel0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NPU firmware&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.1.2.64&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel&lt;/td&gt;
&lt;td&gt;7.1.9-arch1 (&lt;code&gt;amdxdna&lt;/code&gt; in-tree)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;Omarchy (Arch Linux)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AMD quotes the Strix Point NPU at &lt;a href="https://www.amd.com/en/products/processors/laptop/ryzen/ai-300-series/amd-ryzen-ai-9-365.html" rel="noopener noreferrer"&gt;up to 50 TOPS, INT8&lt;/a&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. The driver stack
&lt;/h2&gt;

&lt;p&gt;Three pieces have to be in place before any runtime can touch the NPU:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;amdxdna&lt;/code&gt;&lt;/strong&gt; — the kernel driver. In-tree from Linux 6.14; it's what creates
&lt;code&gt;/dev/accel/accel0&lt;/code&gt;. Check it's bound:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;   $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /dev/accel/
&lt;span class="go"&gt;   accel0
&lt;/span&gt;&lt;span class="gp"&gt;   $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dmesg | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; amdxdna
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;XRT&lt;/strong&gt; (Xilinx/AMD Runtime) + the &lt;strong&gt;&lt;code&gt;xrt-plugin-amdxdna&lt;/code&gt;&lt;/strong&gt; shim. XRT is the
userspace API; the plugin teaches it about the XDNA device. On Arch both are
in &lt;code&gt;extra&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;   $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-S&lt;/span&gt; xrt xrt-plugin-amdxdna
&lt;span class="gp"&gt;   $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;xrt-smi examine
&lt;span class="c"&gt;   ...
&lt;/span&gt;&lt;span class="go"&gt;   XRT
     Version              : 2.21.75
     NPU Firmware Version  : 1.1.2.64

   Device(s) Present
   |BDF             |Name          |
   |----------------|--------------|
   |[0000:66:00.1]  |RyzenAI-npu4  |
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You want a &lt;code&gt;Device(s) Present&lt;/code&gt; line with a &lt;code&gt;RyzenAI-npu*&lt;/code&gt; name. If XRT is&lt;br&gt;
   installed but the plugin isn't, &lt;code&gt;xrt-smi&lt;/code&gt; runs but that table is empty.&lt;br&gt;
   &lt;code&gt;xrt-smi examine --report platform&lt;/code&gt; then shows &lt;code&gt;Total Columns : 8&lt;/code&gt; — the&lt;br&gt;
   XDNA2 array this SoC exposes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Versions that worked here:&lt;/strong&gt; &lt;code&gt;xrt 2.21.75&lt;/code&gt;, &lt;code&gt;xrt-plugin-amdxdna&lt;/code&gt; (same
release), NPU firmware &lt;code&gt;1.1.2.64&lt;/code&gt;, and &lt;code&gt;flm validate&lt;/code&gt; reporting the &lt;code&gt;amdxdna&lt;/code&gt;
driver interface as &lt;strong&gt;0.8&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  The one real gotcha: memlock
&lt;/h3&gt;

&lt;p&gt;The NPU runtime pins model weights into physical RAM, so the calling user needs&lt;br&gt;
an &lt;strong&gt;unlimited memlock rlimit&lt;/strong&gt;. The default (usually 8 MiB or 64 MiB) is nowhere&lt;br&gt;
near enough and the failure mode is an unhelpful allocation error deep in the&lt;br&gt;
runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/security/limits.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt; soft memlock unlimited"&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/security/limits.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt; hard memlock unlimited"&lt;/span&gt;
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;log out and back &lt;span class="k"&gt;in&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want to see this afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ulimit&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="go"&gt;unlimited
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. The shortcut: FastFlowLM instead of the Ryzen AI SDK
&lt;/h2&gt;

&lt;p&gt;The "official" Linux route is: build ONNX Runtime with the VitisAI EP, install&lt;br&gt;
the Ryzen AI SDK bits, quantize your model to the NPU's format, wrangle a Python&lt;br&gt;
venv full of &lt;code&gt;onnxruntime-vitisai&lt;/code&gt; and Vitis tooling. It's a lot, and much of it&lt;br&gt;
is Windows-first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://fastflowlm.com/" rel="noopener noreferrer"&gt;FastFlowLM&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;flm&lt;/code&gt;) skips all of that. It's a&lt;br&gt;
NPU-first runtime (Rust/C++) that ships &lt;strong&gt;prebuilt xclbins&lt;/strong&gt; (the NPU binary&lt;br&gt;
kernels) and &lt;code&gt;libwhisper_npu.so&lt;/code&gt; in the package itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /usr/share/flm/xclbins/
&lt;span class="go"&gt;encoder_attn  encoder_dequant  encoder_mm  whisper_head  ...
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;flm &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;FLM v1.0.2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the kernels are bundled, &lt;strong&gt;there is no onnxruntime-vitisai / Ryzen AI SDK&lt;br&gt;
venv to build&lt;/strong&gt;. (Arch's stock &lt;code&gt;python-onnxruntime-cpu&lt;/code&gt; only has&lt;br&gt;
&lt;code&gt;CPUExecutionProvider&lt;/code&gt; anyway — irrelevant here.) On Arch: &lt;code&gt;sudo pacman -S&lt;br&gt;
fastflowlm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Validate the whole stack in one shot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;flm validate
&lt;span class="go"&gt;[Linux]  Kernel: 7.1.9-arch1-2
[Linux]  NPU: /dev/accel/accel0 with 8 columns
[Linux]  NPU FW Version: 1.1.2.64
[Linux]  amdxdna version: 0.8
[Linux]  Memlock Limit: infinity
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All green = ready. If &lt;code&gt;Memlock Limit&lt;/code&gt; says anything other than &lt;code&gt;infinity&lt;/code&gt;, go&lt;br&gt;
back to the limits.conf step.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Whisper on the NPU
&lt;/h2&gt;

&lt;p&gt;Pull the model — &lt;code&gt;whisper-v3:turbo&lt;/code&gt; is &lt;code&gt;large-v3-turbo&lt;/code&gt; quantized for XDNA2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;flm pull whisper-v3:turbo
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~650 MB: model.q4nx + tokenizers -&amp;gt; ~/.config/flm/models/Whisper-V3-Turbo-NPU2/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serve it. On FLM 1.0.2+ Whisper loads &lt;strong&gt;standalone&lt;/strong&gt; — older docs claimed you had&lt;br&gt;
to co-load an LLM, but you don't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;flm serve &lt;span class="nt"&gt;--asr&lt;/span&gt; 1          &lt;span class="c"&gt;# OpenAI-compatible server on :52625&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transcribe over the HTTP API (anything &lt;code&gt;ffmpeg&lt;/code&gt; can decode — wav/mp3/ogg/m4a/flac):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl http://127.0.0.1:52625/v1/audio/transcriptions &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -F "file=@audio.ogg" \
    -F "model=whisper-v3"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a CLI path: &lt;code&gt;flm run &amp;lt;model&amp;gt; --asr 1&lt;/code&gt;, then &lt;code&gt;/input "clip.mp3"&lt;/code&gt; in&lt;br&gt;
the chat prompt.&lt;/p&gt;
&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;Benchmarked with the bundled &lt;code&gt;bench.py&lt;/code&gt; — 10 runs, first 2 discarded as warm-up,&lt;br&gt;
audio length read from the file via &lt;code&gt;ffprobe&lt;/code&gt; so the RTF is honest and&lt;br&gt;
reproducible:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Audio length&lt;/td&gt;
&lt;td&gt;30.0 s (JFK, Rice University speech excerpt)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transcription wall time (warm)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;5.2 s&lt;/strong&gt; (σ 0.04 s within a run; 5.17–5.6 s across sessions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Real-time factor (RTF)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ 0.17–0.19&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transcript accuracy&lt;/td&gt;
&lt;td&gt;correct, verbatim&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Roughly &lt;strong&gt;5–6× faster than real time&lt;/strong&gt;. Within a single benchmark the spread is&lt;br&gt;
under 1%; between sessions the mean drifts a few hundred ms with machine&lt;br&gt;
temperature and background load.&lt;/p&gt;
&lt;h3&gt;
  
  
  Is it actually on the NPU?
&lt;/h3&gt;

&lt;p&gt;Two checks. First, the FLM log prints &lt;code&gt;[NPU Locked!]&lt;/code&gt; when a job starts and&lt;br&gt;
&lt;code&gt;[NPU Lock Released!]&lt;/code&gt; when it finishes. Second — and more convincing — sample&lt;br&gt;
system load while the benchmark runs and see that nothing else is doing the&lt;br&gt;
work:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device&lt;/th&gt;
&lt;th&gt;Idle baseline&lt;/th&gt;
&lt;th&gt;During 10 transcriptions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU (20 threads, system-wide)&lt;/td&gt;
&lt;td&gt;2.2 %&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;4.3 %&lt;/strong&gt; mean, 14.4 % peak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iGPU (Radeon 890M)&lt;/td&gt;
&lt;td&gt;7 %&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;10 %&lt;/strong&gt; mean, 15 % peak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dGPU (RTX 4070)&lt;/td&gt;
&lt;td&gt;0 %&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 %&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The CPU rises about two points over idle — that's the &lt;code&gt;curl&lt;/code&gt;/harness overhead and&lt;br&gt;
the server's I/O thread, not inference. The iGPU delta is desktop compositing&lt;br&gt;
(Hyprland renders on the 890M), and the discrete GPU is never touched at all.&lt;br&gt;
The 30 seconds of audio is being processed somewhere that doesn't show up in any&lt;br&gt;
of these three counters, which is exactly the point: the CPU and both GPUs stay&lt;br&gt;
free while the NPU works.&lt;/p&gt;
&lt;h3&gt;
  
  
  What the NPU actually buys you: energy
&lt;/h3&gt;

&lt;p&gt;Speed alone isn't the story — &lt;code&gt;whisper-large-v3-turbo&lt;/code&gt; has a tiny decoder and&lt;br&gt;
runs fine on CPU. So I built &lt;code&gt;whisper.cpp&lt;/code&gt; from source (the Arch package's ggml&lt;br&gt;
backend is currently broken) and ran the &lt;strong&gt;same 30 s clip through the same&lt;br&gt;
model&lt;/strong&gt; on the CPU, tuned to 16 threads, reading the RAPL energy counters&lt;br&gt;
(&lt;code&gt;/sys/class/powercap/intel-rapl:0&lt;/code&gt;) around every run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;NPU&lt;/strong&gt; (FastFlowLM)&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;CPU&lt;/strong&gt; (whisper.cpp, &lt;code&gt;-t 16&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wall time (30 s clip)&lt;/td&gt;
&lt;td&gt;~5.3 s&lt;/td&gt;
&lt;td&gt;~6.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTF&lt;/td&gt;
&lt;td&gt;0.18&lt;/td&gt;
&lt;td&gt;0.22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU-package power while running&lt;/td&gt;
&lt;td&gt;~20 W&lt;/td&gt;
&lt;td&gt;~73 W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU-&lt;em&gt;core&lt;/em&gt; power while running&lt;/td&gt;
&lt;td&gt;~0.8 W&lt;/td&gt;
&lt;td&gt;~10 W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Energy per transcription, over idle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~45 J&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~410 J&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Energy per transcription, total package&lt;/td&gt;
&lt;td&gt;~105 J&lt;/td&gt;
&lt;td&gt;~478 J&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The wall-clock win is modest — about 25%. The &lt;strong&gt;energy&lt;/strong&gt; difference is the&lt;br&gt;
point: transcribing that clip on the NPU costs roughly &lt;strong&gt;an order of magnitude&lt;br&gt;
less energy&lt;/strong&gt; than doing it on the CPU (~45 J vs ~410 J above idle). Package&lt;br&gt;
power rises ~10 W instead of ~60 W, the CPU cores never leave idle, and the fans&lt;br&gt;
stay quiet. Per hundred transcriptions that's about 1 W·h versus 11 W·h — and 20&lt;br&gt;
CPU threads left free the whole time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Measured on a live desktop, so absolute wattages drift a few watts between&lt;br&gt;
runs with background activity — "energy over idle" is the stable figure and what&lt;br&gt;
the comparison rests on. &lt;code&gt;whisper-cli&lt;/code&gt; also reloads the 1.6 GB model each run,&lt;br&gt;
which pads its wall time slightly but not its energy. Both harnesses are in the&lt;br&gt;
&lt;a href="https://github.com/jac-76/npu-whisper" rel="noopener noreferrer"&gt;npu-whisper&lt;/a&gt; repo:&lt;br&gt;
&lt;code&gt;bench.py --power&lt;/code&gt; for the NPU column, &lt;code&gt;bench_cpu.py&lt;/code&gt; for the CPU column.)&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  4. An LLM on the same NPU
&lt;/h2&gt;

&lt;p&gt;FLM serves LLMs on the NPU through the same OpenAI-compatible surface. Its model&lt;br&gt;
catalogue covers the usual small-to-mid open weights:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;flm list
&lt;span class="go"&gt;  gemma3:1b        ✅
  qwen3:1.7b       ⏬
  llama3.2:3b      ⏬
  phi4-mini-it:4b  ⏬
  deepseek-r1:8b   ⏬
  gpt-oss:20b      ⏬
  whisper-v3:turbo ✅
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One server can expose &lt;strong&gt;both&lt;/strong&gt; ASR and chat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;flm serve gemma3:1b &lt;span class="nt"&gt;--asr&lt;/span&gt; 1
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl http://127.0.0.1:52625/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -H 'content-type: application/json' \
    -d '{"model":"gemma3:1b","messages":[{"role":"user","content":"hello"}]}'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;/v1/audio/transcriptions&lt;/code&gt; and &lt;code&gt;/v1/chat/completions&lt;/code&gt; are both live on&lt;br&gt;
&lt;code&gt;:52625&lt;/code&gt; from a single process on the NPU.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Tying it together — two small tools
&lt;/h2&gt;

&lt;p&gt;With the endpoint working, the rest is glue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/jac-76/npu-whisper" rel="noopener noreferrer"&gt;&lt;code&gt;npu-whisper&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — a zero-dependency&lt;br&gt;
Bash wrapper: run it on an audio file and it auto-starts &lt;code&gt;flm serve --asr 1&lt;/code&gt;&lt;br&gt;
if it's down, waits for readiness, &lt;code&gt;curl&lt;/code&gt;s the transcript, and leaves the&lt;br&gt;
server warm. &lt;code&gt;--json&lt;/code&gt;, &lt;code&gt;--status&lt;/code&gt;, &lt;code&gt;--stop&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/jac-76/local-ai-assistant" rel="noopener noreferrer"&gt;&lt;code&gt;local-ai-assistant&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — a&lt;br&gt;
fully offline voice assistant, stdlib-only Python:&lt;br&gt;
&lt;code&gt;pw-record → Whisper (NPU) → LLM (NPU) → piper TTS → speaker&lt;/code&gt;. One FLM server&lt;br&gt;
backs the whole chain. &lt;code&gt;chat&lt;/code&gt; keeps conversation history across runs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are deliberately small — the interesting work was getting the NPU to do the&lt;br&gt;
inference, not the plumbing on top.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotchas &amp;amp; rough edges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;memlock&lt;/strong&gt; is the thing that bites everyone first. &lt;code&gt;flm validate&lt;/code&gt; catches it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FLM version:&lt;/strong&gt; 1.0.2 here; 1.0.3 exists but the update notice points at a
Windows &lt;code&gt;.msi&lt;/code&gt; — 1.0.2 is fine to stay on for Linux.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model format is runtime-specific.&lt;/strong&gt; These &lt;code&gt;.q4nx&lt;/code&gt; weights + bundled xclbins
are FastFlowLM's; you can't point llama.cpp or vanilla ONNX Runtime at them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No GPU/CPU fallback worth using.&lt;/strong&gt; If the NPU path fails, you're better off
fixing it than limping along on CPU — &lt;code&gt;whisper.cpp&lt;/code&gt; on CPU is far slower for
&lt;code&gt;large-v3-turbo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discoverability.&lt;/strong&gt; Almost every AMD NPU tutorial is Windows. The Linux
&lt;code&gt;amdxdna&lt;/code&gt; + XRT + FLM combination works well but you have to assemble it
yourself. That's the gap this post is trying to close.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;1. driver stack
&lt;span class="go"&gt;sudo pacman -S xrt xrt-plugin-amdxdna fastflowlm
&lt;/span&gt;&lt;span class="gp"&gt;sudo tee -a /etc/security/limits.conf &amp;lt;&amp;lt;&amp;lt; "$&lt;/span&gt;USER soft memlock unlimited&lt;span class="s2"&gt;"
&lt;/span&gt;&lt;span class="gp"&gt;sudo tee -a /etc/security/limits.conf &amp;lt;&amp;lt;&amp;lt; "$&lt;/span&gt;&lt;span class="s2"&gt;USER hard memlock unlimited"&lt;/span&gt;
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;log out / back &lt;span class="k"&gt;in&lt;/span&gt;
&lt;span class="gp"&gt;flm validate            #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;want: all green, Memlock Limit: infinity
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;2. models
&lt;span class="go"&gt;flm pull whisper-v3:turbo
flm pull gemma3:1b

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;3. run
&lt;span class="go"&gt;flm serve gemma3:1b --asr 1
curl http://127.0.0.1:52625/v1/audio/transcriptions -F file=@clip.wav -F model=whisper-v3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requirements: a Ryzen AI (XDNA / XDNA2) laptop, kernel ≥ 6.14 with &lt;code&gt;amdxdna&lt;/code&gt;,&lt;br&gt;
and the memlock bump.&lt;/p&gt;

</description>
      <category>amd</category>
      <category>ryzenai</category>
      <category>npu</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
