<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>TinyComputers.io (Posts about rocm)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/rocm.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 A.C. Jokela 
&lt;!-- div style="width: 100%" --&gt;
&lt;a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"&gt;&lt;img alt="" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png" /&gt; Creative Commons Attribution-ShareAlike&lt;/a&gt;&amp;nbsp;|&amp;nbsp;
&lt;!-- /div --&gt;
</copyright><lastBuildDate>Thu, 11 Jun 2026 00:43:18 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Running DiffusionGemma on AMD Strix Halo and Decade-Old Tesla P40s</title><link>https://tinycomputers.io/posts/running-diffusiongemma-on-strix-halo-and-tesla-p40s.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/running-diffusiongemma-on-strix-halo-and-tesla-p40s_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;25 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Google released &lt;a href="https://baud.rs/KxTPsM"&gt;DiffusionGemma&lt;/a&gt; this week, and it's the most interesting thing to come out of the Gemma program in a while — not because it's bigger or smarter than what came before, but because it generates text in a fundamentally different way. Instead of predicting one token at a time, left to right, the way every mainstream LLM has worked since GPT-2, DiffusionGemma starts with a canvas of 256 masked tokens and iteratively denoises the whole block at once. Google's headline claim is up to 4x faster generation: over 1,000 tokens per second on an H100, 700+ on an RTX 5090.&lt;/p&gt;
&lt;p&gt;I do not own an H100. I own an &lt;a href="https://tinycomputers.io/posts/running-deepseek-v4-flash-on-amd-strix-halo.html"&gt;AMD Strix Halo APU&lt;/a&gt; and a rack-mount server with &lt;a href="https://tinycomputers.io/posts/repurposing-enterprise-gpus-the-tesla-p40-home-lab-story.html"&gt;four NVIDIA Tesla P40s&lt;/a&gt; from 2016. Neither appears anywhere in Google's launch material, and for good reason — the official deployment paths assume hardware features that one of these machines lacks entirely and the other only half-has. Getting DiffusionGemma running on both took an unmerged llama.cpp pull request, a community quantization, and the usual amount of stubbornness.&lt;/p&gt;
&lt;p&gt;It runs on both. And the benchmark result surprised me: the integrated GPU in a mini PC beat four discrete NVIDIA server cards — 96GB of combined VRAM, roughly 48 TFLOPS of aggregate FP32 — by a factor of more than two. This post covers the setup on each machine, the head-to-head numbers, and why diffusion inference inverts some of the assumptions I've built up over years of running autoregressive models on this hardware.&lt;/p&gt;
&lt;h3&gt;What Block Diffusion Actually Does&lt;/h3&gt;
&lt;p&gt;DiffusionGemma is built on the Gemma 4 26B A4B architecture: a Mixture-of-Experts model with 25.2 billion total parameters, of which only 3.8 billion are active for any given token. The MoE part is familiar. The generation process is not.&lt;/p&gt;
&lt;p&gt;An autoregressive model produces text serially. Each new token requires a full forward pass conditioned on everything before it, so a 256-token response means 256 sequential passes through the network. The KV cache makes each pass cheap, but the serial dependency is structural: token N cannot begin until token N-1 is finished. Generation speed is dominated by how fast you can stream weights through the GPU's memory bus, over and over.&lt;/p&gt;
&lt;p&gt;DiffusionGemma instead works on a 256-token block it calls a canvas. Every position starts as a mask token. Each denoising step runs one forward pass over the &lt;em&gt;entire&lt;/em&gt; canvas and proposes tokens for every position simultaneously. Positions where the model is confident get committed; uncertain ones stay masked for the next round. The sampler in the released implementation is called entropy-bound: rather than unmasking a fixed number of tokens per step, it commits every position whose predicted distribution has entropy below a threshold, which means easy spans of text resolve in a handful of steps while tricky ones get more iterations. For sequences longer than one canvas, the model chains blocks autoregressively — each new 256-token canvas conditions on the completed text before it. Google calls this block-autoregressive multi-canvas sampling, which is a lot of words for "diffusion inside the block, autoregression between blocks."&lt;/p&gt;
&lt;p&gt;The performance implication is the whole point. If a canvas resolves in 20 denoising steps, you've produced 256 tokens with 20 forward passes instead of 256. The trade is that each pass is much heavier: you're computing attention and expert routing for 2,300-odd positions of context-plus-canvas every step, not one new position. Diffusion converts text generation from a memory-bandwidth-bound serial problem into a compute-bound parallel one.&lt;/p&gt;
&lt;p&gt;Keep that sentence in mind. It decides the entire benchmark.&lt;/p&gt;
&lt;h3&gt;The Official Paths Don't Fit&lt;/h3&gt;
&lt;p&gt;The weights are on Hugging Face under Apache 2.0 as &lt;code&gt;google/diffusiongemma-26B-A4B-it&lt;/code&gt; — about 50GB in BF16. Google's developer guide shows a vLLM serving command and notes support in Transformers, SGLang, and MLX, with quantized deployment fitting in 18GB of VRAM.&lt;/p&gt;
&lt;p&gt;None of that helps either of my machines.&lt;/p&gt;
&lt;p&gt;The P40 problem is the same one I keep running into with this hardware. vLLM requires compute capability 7.0 or higher; the P40's Pascal GP102 die is 6.1, so vLLM won't even initialize. The Transformers path technically exists, but the weights are BF16 — a format Pascal has no hardware support for — and the fallback, FP16, runs on the P40 at 1/64th the rate of FP32 because Pascal's half-precision units were an afterthought on this die. I've &lt;a href="https://tinycomputers.io/posts/running-ltx-video-on-four-tesla-p40s.html"&gt;written before&lt;/a&gt; about the contortions required to run BF16-native models on these cards, and a 25-billion-parameter model in FP32 would need 100GB — more than the 96GB the four cards have between them.&lt;/p&gt;
&lt;p&gt;The Strix Halo could plausibly run the Transformers path, since ROCm's PyTorch handles BF16 on RDNA 3.5 fine. But a 50GB BF16 model plus activations through HuggingFace Transformers on an iGPU is the slow, painful version of this experiment. What both machines actually want is what they always want: llama.cpp and a good GGUF.&lt;/p&gt;
&lt;p&gt;The launch blog said llama.cpp support was "coming soon." Soon turned out to already be in flight: pull request &lt;a href="https://baud.rs/PDycIw"&gt;#24423&lt;/a&gt; on the llama.cpp repository implements the DiffusionGemma architecture and a dedicated &lt;code&gt;llama-diffusion-cli&lt;/code&gt; binary, and the Unsloth team had already published GGUF quantizations built against it — the kind of community velocity that has become the norm within days of any open-weights release. The PR is unmerged as I write this, which means building it yourself, but that's hardly a hardship.&lt;/p&gt;
&lt;p&gt;The Unsloth repository offers the usual ladder:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quantization&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;50.5 GB&lt;/td&gt;
&lt;td&gt;Full precision reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;26.9 GB&lt;/td&gt;
&lt;td&gt;Near-lossless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;22.7 GB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;19.1 GB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;16.8 GB&lt;/td&gt;
&lt;td&gt;Fits a single 24GB card&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I went with Q8_0 on both machines. The P40 box has 96GB of VRAM to spread it across, the Strix Halo has 121GB of unified memory, and for a model whose output quality is already documented as a step below standard Gemma 4, I didn't want quantization noise muddying the comparison.&lt;/p&gt;
&lt;h3&gt;Machine One: Four Tesla P40s&lt;/h3&gt;
&lt;p&gt;The P40 server is the familiar workhorse: four GP102GL cards with 24GB of GDDR5X each, Ubuntu 24.04, the NVIDIA 580-series driver, and a 64-core host. It already had a llama.cpp checkout from December, with a working CUDA build configured for &lt;code&gt;CMAKE_CUDA_ARCHITECTURES=61&lt;/code&gt;. Rather than disturb a known-good build, I pulled the PR into a git worktree alongside it:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/llama.cpp
git&lt;span class="w"&gt; &lt;/span&gt;fetch&lt;span class="w"&gt; &lt;/span&gt;origin&lt;span class="w"&gt; &lt;/span&gt;pull/24423/head:diffusiongemma
git&lt;span class="w"&gt; &lt;/span&gt;worktree&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;~/llama.cpp-diffusiongemma&lt;span class="w"&gt; &lt;/span&gt;diffusiongemma

&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/llama.cpp-diffusiongemma
cmake&lt;span class="w"&gt; &lt;/span&gt;-B&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-DGGML_CUDA&lt;span class="o"&gt;=&lt;/span&gt;ON&lt;span class="w"&gt; &lt;/span&gt;-DCMAKE_CUDA_ARCHITECTURES&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;61&lt;/span&gt;
cmake&lt;span class="w"&gt; &lt;/span&gt;--build&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--target&lt;span class="w"&gt; &lt;/span&gt;llama-diffusion-cli
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The worktree trick is worth adopting if you haven't: one clone, multiple checked-out branches in separate directories, no second &lt;code&gt;.git&lt;/code&gt; download, and the original build directory stays untouched. The build target matters too — DiffusionGemma doesn't run under the standard &lt;code&gt;llama-cli&lt;/code&gt; or &lt;code&gt;llama-server&lt;/code&gt;; the diffusion sampling loop lives in its own &lt;code&gt;llama-diffusion-cli&lt;/code&gt; binary.&lt;/p&gt;
&lt;p&gt;The CUDA 12.0 toolkit that ships in Ubuntu 24.04's repositories compiled the sm_61 kernels without complaint. While it built, the Q8_0 GGUF downloaded from Hugging Face. First run:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;./build/bin/llama-diffusion-cli&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;~/models/diffusiongemma-26B-A4B-it-Q8_0.gguf&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-ngl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;99&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Explain why text diffusion models can generate text faster than autoregressive models."&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Two log lines worth noting before the results. First:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;sched_reserve&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;assigned&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CUDA0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;but&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Flash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Attention&lt;/span&gt;
&lt;span class="n"&gt;tensor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;assigned&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CPU&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usually&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;due&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;support&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sched_reserve&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Flash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Attention&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;disabled&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The PR doesn't yet have Flash Attention kernels wired up for this architecture on this hardware, so attention falls back to the unfused path. That's a real performance tax on a workload that's almost entirely attention-over-2,300-positions, and it suggests headroom once the PR matures.&lt;/p&gt;
&lt;p&gt;Second:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;diffusion_eb&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GPUs&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;diffusion&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;force&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The diffusion sampler maintains an optional KV cache over the already-committed prompt prefix, so each denoising step only recomputes attention for the active canvas. With the model split across four GPUs, the PR disables this by default. File that away; it becomes a benchmark variable later.&lt;/p&gt;
&lt;p&gt;And then it worked. Nineteen entropy-bound denoising steps, 42 seconds, and a coherent 256-token answer — produced by a model that was, frankly, fascinating to watch. With &lt;code&gt;--diffusion-visual&lt;/code&gt; you can see the canvas refine in place: scattered high-confidence words appear first, connective tissue fills in around them, and the text snaps into focus over successive steps like a developing photograph. It is the most legible window into "what is the model doing" I've encountered since attention map visualizations stopped being useful.&lt;/p&gt;
&lt;h3&gt;Machine Two: Strix Halo&lt;/h3&gt;
&lt;p&gt;The Strix Halo machine is the same one that ran &lt;a href="https://tinycomputers.io/posts/running-deepseek-v4-flash-on-amd-strix-halo.html"&gt;DeepSeek V4 Flash&lt;/a&gt;: a Ryzen AI MAX+ 395 with the integrated Radeon 8060S (gfx1151, 40 RDNA 3.5 compute units), 128GB of LPDDR5X unified memory, and &lt;a href="https://tinycomputers.io/posts/upgrading-rocm-7.0-to-7.2-on-amd-strix-halo-gfx1151.html"&gt;ROCm 7.2&lt;/a&gt;. The setup mirrors the P40 box almost line for line, with the CUDA flags swapped for HIP:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/llama.cpp
git&lt;span class="w"&gt; &lt;/span&gt;fetch&lt;span class="w"&gt; &lt;/span&gt;--no-tags&lt;span class="w"&gt; &lt;/span&gt;origin&lt;span class="w"&gt; &lt;/span&gt;pull/24423/head:diffusiongemma
git&lt;span class="w"&gt; &lt;/span&gt;worktree&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;~/llama.cpp-diffusiongemma&lt;span class="w"&gt; &lt;/span&gt;diffusiongemma

&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/llama.cpp-diffusiongemma
cmake&lt;span class="w"&gt; &lt;/span&gt;-B&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-DGGML_HIP&lt;span class="o"&gt;=&lt;/span&gt;ON&lt;span class="w"&gt; &lt;/span&gt;-DAMDGPU_TARGETS&lt;span class="o"&gt;=&lt;/span&gt;gfx1151&lt;span class="w"&gt; &lt;/span&gt;-DCMAKE_BUILD_TYPE&lt;span class="o"&gt;=&lt;/span&gt;Release
cmake&lt;span class="w"&gt; &lt;/span&gt;--build&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--target&lt;span class="w"&gt; &lt;/span&gt;llama-diffusion-cli
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Instead of downloading the 26.9GB GGUF a second time, I rsynced it across the LAN from the P40 server. Both machines sit on the same switch, and Hugging Face's CDN doesn't get faster than a local wire.&lt;/p&gt;
&lt;p&gt;One memorable wrinkle: this box's disk was 97% full when I started, with 65GB free — enough for the model, but barely. The subsequent archaeology turned up 329GB of forgotten Ollama models and 118GB of GGUFs cached by root from September experiments, and the cleanup freed almost 600GB. The home lab equivalent of finding grocery money in last winter's coat.&lt;/p&gt;
&lt;p&gt;The HIP build compiled the PR without a single source change — the same commit, &lt;code&gt;c84e85af6&lt;/code&gt;, that built for CUDA sm_61 also built for gfx1151. Whatever else you want to say about the ggml project, its backend abstraction has earned its keep. The same Flash Attention fallback warning appeared, so both machines run the same unfused attention path, which keeps the comparison honest. One difference: as a single-GPU configuration, the Strix Halo got the diffusion KV cache enabled by default.&lt;/p&gt;
&lt;h3&gt;The Benchmark&lt;/h3&gt;
&lt;p&gt;Same model file, same PR commit, same prompt, same flags, two generation lengths. The only differences are the silicon and the KV cache default. The &lt;code&gt;llama-diffusion-cli&lt;/code&gt; output reports total wall time and per-step time directly:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Strix Halo (Radeon 8060S)&lt;/th&gt;
&lt;th&gt;4x Tesla P40&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;256 tokens (1 canvas)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17.4s&lt;/strong&gt; — 17 steps, 1,025 ms/step&lt;/td&gt;
&lt;td&gt;42.5s — 19 steps, 2,235 ms/step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;512 tokens (2 canvases)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37.1s&lt;/strong&gt; — 36 steps, 1,031 ms/step&lt;/td&gt;
&lt;td&gt;92.1s — 38 steps, 2,423 ms/step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective throughput&lt;/td&gt;
&lt;td&gt;~14 tokens/sec&lt;/td&gt;
&lt;td&gt;~5.6 tokens/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The integrated GPU in a mini PC that idles at a few dozen watts is 2.2x faster than four server cards drawing 250 watts apiece. Per denoising step, it's 1.03 seconds versus 2.2–2.4 seconds, and that per-step time holds almost perfectly flat as the generation grows from one canvas to two — 1,025ms to 1,031ms on the AMD side — so longer outputs scale linearly with block count on both machines.&lt;/p&gt;
&lt;p&gt;My first suspect for the gap was the KV cache asymmetry, since the P40 box had it disabled. Easy to test: force it on with &lt;code&gt;--diffusion-kv-cache on&lt;/code&gt; and rerun. Result: 2,179 ms/step versus 2,235 — a 2.5% improvement. Not the answer. The gap is the hardware, and it's worth understanding why, because the explanation is the inverse of every previous benchmark I've run on these two machines.&lt;/p&gt;
&lt;h3&gt;Why the iGPU Wins&lt;/h3&gt;
&lt;p&gt;For autoregressive inference, the P40s' saving grace has always been memory bandwidth. Each card moves about 346 GB/s from GDDR5X, and token-by-token generation is essentially a memory streaming benchmark — which is why these $200 relics have stayed &lt;a href="https://tinycomputers.io/posts/the-economics-of-owning-your-own-inference.html"&gt;economically relevant&lt;/a&gt; for chat workloads years after their compute became obsolete. The Strix Halo's LPDDR5X manages roughly 256 GB/s shared between CPU and GPU, so for ordinary LLM generation the P40s usually hold their own or win.&lt;/p&gt;
&lt;p&gt;Diffusion flips the workload. Every denoising step is one enormous batched forward pass: 2,300+ positions of attention, MoE routing, and expert FFNs computed simultaneously. The weights are read once per step and amortized across all 256 canvas positions, so memory bandwidth stops being the bottleneck. What matters is raw arithmetic throughput on big matrix multiplies — exactly the regime where modern architectures embarrass Pascal.&lt;/p&gt;
&lt;p&gt;Three specific factors stack up against the P40s:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pascal's arithmetic is stuck in 2016.&lt;/strong&gt; No tensor cores, useless FP16, no DP4A path that helps here. Every matmul in the unfused attention and expert layers runs through plain FP32 CUDA cores at GP102's native rate. RDNA 3.5 brings WMMA instructions and dual-issue FP32 — per-clock, per-unit, it simply does more math, and on a compute-bound workload that's the whole game.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Four GPUs synchronize every step.&lt;/strong&gt; llama.cpp splits the model by layers, so each denoising step's forward pass marches through GPU 0, then 1, then 2, then 3, handing activations across PCIe 3.0 at every boundary — for a batch of 2,300 positions, a meaningfully larger transfer than single-token inference ever produces. In autoregressive mode this pipeline overhead hides behind memory streaming; at 48 steps per 512-token generation, it's pure tax. The unified-memory APU pays nothing. It doesn't even cross a PCIe bus to read the weights.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Neither machine has Flash Attention here, but the penalty isn't symmetric.&lt;/strong&gt; Unfused attention materializes large intermediate matrices and burns bandwidth and compute on a canvas-sized sequence every step. The architecture with more arithmetic headroom absorbs that better.&lt;/p&gt;
&lt;p&gt;The result is a benchmark where one of the oldest tricks in the home lab playbook — gang cheap cards together until the VRAM adds up — actively hurts, and the boring little APU that just holds everything in one pool of memory wins by a wide margin. The same dynamic decided the &lt;a href="https://tinycomputers.io/posts/running-deepseek-v4-flash-on-amd-strix-halo.html"&gt;DeepSeek V4 experiment&lt;/a&gt;, but for a different reason: there it was instruction set support; here the P40s run the model &lt;em&gt;correctly&lt;/em&gt; and still lose on the shape of the computation. Ten-year-old hardware doesn't fail all at once. It fails one workload category at a time.&lt;/p&gt;
&lt;h3&gt;Watching It Think&lt;/h3&gt;
&lt;p&gt;A few qualitative observations that the timing table doesn't capture.&lt;/p&gt;
&lt;p&gt;The entropy-bound sampler's step count genuinely varies with content. Across runs I saw single canvases resolve in anywhere from 15 to 19 steps against a configured maximum of 48 — the sampler's confidence and entropy thresholds (&lt;code&gt;t=[0.400, 0.800]&lt;/code&gt;, &lt;code&gt;entropy_bound=0.1&lt;/code&gt;, &lt;code&gt;confidence=0.005&lt;/code&gt; in the defaults) decide when each position commits, so boilerplate prose converges fast while denser passages take more iterations. The practical effect is that generation time depends on how &lt;em&gt;hard&lt;/em&gt; the text is, not just how long. That's a strange property to develop intuitions for after years of fixed per-token costs.&lt;/p&gt;
&lt;p&gt;The instruction-tuned model also produces an explicit planning trace — drafting bullet points, weighing alternatives, revising phrasing — before its final answer, in the now-familiar reasoning-model style. Watching a &lt;em&gt;diffusion&lt;/em&gt; model do this is doubly strange, because the plan and the answer crystallize as blocks rather than as a stream, paragraph-scale thoughts emerging whole.&lt;/p&gt;
&lt;p&gt;And the quality caveat is real, so I'll repeat Google's own framing: DiffusionGemma trades output quality for speed relative to standard Gemma 4. It's an experimental release aimed at speed-critical applications — real-time editing, latency-sensitive drafting — not a production daily driver. On my hardware, which can't reach the speeds that make the trade compelling, it's best understood as a working preview of a genuinely different inference paradigm. That's worth 27GB of disk to me.&lt;/p&gt;
&lt;h3&gt;The Recipe, Condensed&lt;/h3&gt;
&lt;p&gt;For either machine, the full setup is four commands and a download. CUDA flavor:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/ggml-org/llama.cpp
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;llama.cpp
git&lt;span class="w"&gt; &lt;/span&gt;fetch&lt;span class="w"&gt; &lt;/span&gt;origin&lt;span class="w"&gt; &lt;/span&gt;pull/24423/head:diffusiongemma&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;checkout&lt;span class="w"&gt; &lt;/span&gt;diffusiongemma
cmake&lt;span class="w"&gt; &lt;/span&gt;-B&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-DGGML_CUDA&lt;span class="o"&gt;=&lt;/span&gt;ON&lt;span class="w"&gt; &lt;/span&gt;-DCMAKE_CUDA_ARCHITECTURES&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;61&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# 61 = Pascal/P40&lt;/span&gt;
cmake&lt;span class="w"&gt; &lt;/span&gt;--build&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;--target&lt;span class="w"&gt; &lt;/span&gt;llama-diffusion-cli
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;ROCm flavor, swap the configure line:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;cmake&lt;span class="w"&gt; &lt;/span&gt;-B&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-DGGML_HIP&lt;span class="o"&gt;=&lt;/span&gt;ON&lt;span class="w"&gt; &lt;/span&gt;-DAMDGPU_TARGETS&lt;span class="o"&gt;=&lt;/span&gt;gfx1151&lt;span class="w"&gt; &lt;/span&gt;-DCMAKE_BUILD_TYPE&lt;span class="o"&gt;=&lt;/span&gt;Release
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then grab a GGUF from &lt;code&gt;unsloth/diffusiongemma-26B-A4B-it-GGUF&lt;/code&gt; on Hugging Face — Q8_0 if you have 27GB of memory to spend, Q4_K_M if you're fitting a single 24GB card — and run:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;./build/bin/llama-diffusion-cli&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;diffusiongemma-26B-A4B-it-Q8_0.gguf&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-ngl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;99&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-cnv&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2048&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--diffusion-visual
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;--diffusion-visual&lt;/code&gt; flag is optional and you should absolutely use it anyway. Once the PR merges into mainline llama.cpp, the fetch-and-checkout step disappears and this becomes as routine as running any other GGUF.&lt;/p&gt;
&lt;p&gt;The deeper takeaway from this experiment isn't about DiffusionGemma specifically. It's that inference hardware assumptions are workload assumptions in disguise. The P40s survive in my rack because autoregressive generation is kind to old silicon with decent memory bandwidth. The first mainstream model family to change &lt;em&gt;how&lt;/em&gt; tokens get generated — not just how many parameters produce them — quietly rewrote that bargain. If text diffusion graduates from experiment to standard practice, the hardware hierarchy of the home lab gets reshuffled, and the winners will be whatever has the most matmul per dollar, not the most gigabytes per second. I suspect the P40s will still find work. They always do. But I've stopped assuming I know which jobs they'll be good at.&lt;/p&gt;</description><category>amd</category><category>cuda</category><category>diffusiongemma</category><category>gemma</category><category>gfx1151</category><category>gguf</category><category>home lab</category><category>inference</category><category>llama.cpp</category><category>llm</category><category>machine learning</category><category>moe</category><category>open-source</category><category>pascal</category><category>quantization</category><category>rocm</category><category>strix halo</category><category>tesla p40</category><category>text diffusion</category><guid>https://tinycomputers.io/posts/running-diffusiongemma-on-strix-halo-and-tesla-p40s.html</guid><pubDate>Wed, 10 Jun 2026 23:30:00 GMT</pubDate></item><item><title>Teaching a Transformer to Write Z80 Assembly: Why Supervised Learning Crushed Reinforcement Learning</title><link>https://tinycomputers.io/posts/teaching-a-transformer-to-write-z80-assembly.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/teaching-a-transformer-to-write-z80-assembly_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;41 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;There is a particular kind of hubris in thinking you can teach a neural network to write assembly language. Assembly is not forgiving. There are no helpful type errors, no compiler warnings, no second chances. You emit bytes — 0x3E means load a constant into the accumulator, 0x87 means add the accumulator to itself — and if you get even one byte wrong, the CPU executes something you did not intend. Usually it executes garbage. Sometimes it executes nothing at all. Either way, you fail.&lt;/p&gt;
&lt;p&gt;I spent the better part of a weekend trying to make reinforcement learning teach a transformer to generate Z80 assembly. The transformer was 228 million parameters, trained on a corpus of scraped Z80 source code, then fine-tuned with REINFORCE and later PPO using a cycle-accurate Rust emulator as the reward signal. The idea was elegant: the model generates bytecode, the emulator executes it, a reward function scores the result based on correctness, cycle count, and code size, and the policy gradient pushes the model toward faster, smaller programs.&lt;/p&gt;
&lt;p&gt;It did not work. At all. Across six different configurations, the RL-trained model never exceeded single-digit accuracy, and usually collapsed to generating empty programs or crashing the emulator with invalid instruction encodings. When I finally gave up on RL and switched to pure supervised learning with auto-generated ground truth data, accuracy jumped from zero to one hundred percent on a sixteen-task benchmark spanning eight categories of Z80 optimization.&lt;/p&gt;
&lt;p&gt;This post is about why. It is about the shape of reward landscapes, the surprising power of synthetic training data, and the lesson that better representations beat better algorithms every time.&lt;/p&gt;
&lt;h3&gt;The Problem: Generate Optimized Z80 Bytecode&lt;/h3&gt;
&lt;p&gt;The task is deceptively simple. Given a specification — task type, input register values, expected output — generate a sequence of Z80 machine code bytes that correctly transforms the inputs into the expected output. The code should not just work; it should be &lt;em&gt;good&lt;/em&gt;. Fewer clock cycles, fewer bytes, smarter use of side effects. The kind of thing a human Z80 programmer does by instinct, encoded in a loss function.&lt;/p&gt;
&lt;p&gt;I defined sixteen test tasks across eight categories of increasing difficulty:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Constant folding&lt;/strong&gt; (difficulty 0.1–0.2): arithmetic expressions where the operands are known at compile time. The model should emit the precomputed result as an immediate load. "Compute A = 5 + 3" becomes &lt;code&gt;LD A, 8&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Register allocation&lt;/strong&gt; (0.3–0.4): moving values between registers without touching memory. "Swap A and B" should use a temporary register or direct exchange.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Peephole optimization&lt;/strong&gt; (0.3–0.4): eliminating redundant instructions. "LD A, 0; ADD A, B" should collapse to "LD A, B" because the zero load is dead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Loop unrolling&lt;/strong&gt; (0.5): expanding counted loops into straight-line code. Summing four bytes at (HL) is faster with four explicit &lt;code&gt;ADD A, (HL); INC HL&lt;/code&gt; instructions than a &lt;code&gt;DJNZ&lt;/code&gt; loop.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flag-aware rewriting&lt;/strong&gt; (0.3–0.6): exploiting flag side effects. &lt;code&gt;CP 0&lt;/code&gt; is seven cycles; &lt;code&gt;OR A&lt;/code&gt; sets the zero flag in four.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Memory copy&lt;/strong&gt; (0.5): block transfers. &lt;code&gt;LDIR&lt;/code&gt; copies BC bytes from HL to DE in a single instruction instead of a byte-at-a-time loop.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bit manipulation&lt;/strong&gt; (0.4–0.5): using logical operations instead of dedicated bit instructions. Setting bit three of A is &lt;code&gt;OR 0x08&lt;/code&gt; (seven cycles) versus &lt;code&gt;SET 3, A&lt;/code&gt; (eight cycles).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Arithmetic chains&lt;/strong&gt; (0.6–0.7): multi-step computations. &lt;code&gt;(A + B) * 2 - C&lt;/code&gt; requires add, double, subtract — in the right order, with the right registers.&lt;/p&gt;
&lt;p&gt;The benchmark is challenging because it spans fundamentally different instruction patterns. A model that memorizes &lt;code&gt;LD A, constant&lt;/code&gt; for constant folding tasks can't apply that same template to a 16-bit addition that requires &lt;code&gt;LD H, B; LD L, C; ADD HL, DE&lt;/code&gt;. It has to learn a compositional mapping from problem structure to instruction sequence.&lt;/p&gt;
&lt;h3&gt;The Architecture&lt;/h3&gt;
&lt;p&gt;The model is a decoder-only transformer. Not a large one by modern standards — 51 million parameters in its final configuration, with a model dimension of 512, twelve layers, and sixteen attention heads. It autoregressively generates raw Z80 bytecode tokens (0–255 plus special BOS and EOS tokens) given a task specification vector.&lt;/p&gt;
&lt;p&gt;The task specification is a concatenation of the task type — an integer from 0 to 15 — and up to eight operand values representing the initial register state. For a constant folding task like "A = 5 + 3", the context is: type=0, operands=[5, 0, 0, 0, 0, 0, 0, 0]. The model sees this, then generates tokens like &lt;code&gt;0x3E 0x08 0x32 0x00 0x80 0x76&lt;/code&gt; — &lt;code&gt;LD A, 8; LD (0x8000), A; HALT&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The execution environment is a real Z80 emulator. I wrote a Rust wrapper around the &lt;code&gt;rz80&lt;/code&gt; crate that accepts bytecode via JSON over stdin, initializes registers and memory, executes with a cycle budget, and returns the final register state, total T-states consumed, and a memory snapshot at the output address. This is not a toy simulator — it's a cycle-accurate emulation of a complete Z80 CPU with 64KB of RAM. Every instruction takes exactly the number of T-states the real hardware would consume. The reward function has access to ground-truth timing data.&lt;/p&gt;
&lt;p&gt;The reward function itself is straightforward:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;correct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;  &lt;span class="c1"&gt;# base correctness bonus&lt;/span&gt;
    &lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;cycles_saved&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;  &lt;span class="c1"&gt;# efficiency bonus&lt;/span&gt;
    &lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;bytes_saved&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;  &lt;span class="c1"&gt;# compactness bonus&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cycles_saved&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;bytes_saved&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;  &lt;span class="c1"&gt;# Pareto improvement bonus&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;hamming_match_ratio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# partial credit&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The magnitudes are tuned to make correctness dominant: getting the right answer is worth at least +10, while the most you can gain from efficiency is a few additional points. Getting the wrong answer costs you at least -5 regardless of how clever your code is. This is important because it means the reward function has exactly one spike at the correct solution, with a crater of negative reward everywhere else. There is no gradient, no partial improvement, no hill to climb. You either produce the right output bytes or you don't.&lt;/p&gt;
&lt;h3&gt;Attempt 1: REINFORCE with a 228M Model&lt;/h3&gt;
&lt;p&gt;The first attempt was the most ambitious. I loaded a 228-million-parameter model pre-trained on a corpus of scraped Z80 assembly source — GitHub repositories full of &lt;code&gt;.asm&lt;/code&gt;, &lt;code&gt;.z80&lt;/code&gt;, and &lt;code&gt;.s&lt;/code&gt; files from CP/M implementations, ZX Spectrum programs, and retro operating systems. The idea was that the model would develop an internal representation of Z80 instruction semantics from raw text, which RL could then shape into bytecode generation.&lt;/p&gt;
&lt;p&gt;I immediately hit a problem. The pre-trained model had learned Z80 mnemonics as tokens — &lt;code&gt;LD&lt;/code&gt;, &lt;code&gt;ADD&lt;/code&gt;, &lt;code&gt;PUSH&lt;/code&gt; — mapped to token IDs in the 256+ range. But the RL environment needed raw byte opcodes, which live in the 0–255 range. The model's byte-level embeddings were essentially noise; when constrained to output only byte tokens during RL, the model generated EOS immediately. Empty programs. Zero bytes.&lt;/p&gt;
&lt;p&gt;The fix was to reinitialize the output projection layer and token embeddings while keeping the transformer body. This gave the model random byte output at the start, letting the policy gradient shape it from scratch. The transformer layers retained whatever structural knowledge of Z80 assembly they had absorbed during pre-training.&lt;/p&gt;
&lt;p&gt;The result: zero percent accuracy on the benchmark. Not just at epoch one — at epoch forty-six, after more than nine thousand episodes of RL training. The model oscillated between five and nine percent correct, never improving. The code size hovered around 190–200 bytes, which is the max sequence length of 256 minus the store-and-halt suffix. The model had learned exactly one thing: fill the output buffer with random bytes and hope for the best.&lt;/p&gt;
&lt;p&gt;The problem was fundamental. REINFORCE distributes the terminal reward equally across all generated tokens. A 200-byte program that happens to put the right value at the output address gets a +10 reward, split into +0.05 per token. A 200-byte program that doesn't gets -5, split into -0.025 per token. With a 256-token vocabulary, the probability of generating a correct 6-byte program by chance is approximately (1/256)^6 ≈ 3.5 × 10^-15. The model never explores enough to find correct sequences, so the reward signal is dominated by -5 penalties that push the policy in a random direction each epoch. The policy performs a random walk around whatever initialization it started with, occasionally stumbling into a correct program by accident, briefly getting a positive signal, then immediately being pushed back into noise by the next batch of negative rewards.&lt;/p&gt;
&lt;p&gt;The code was correct. The emulator was correct. The reward function was correct. The algorithm — REINFORCE applied to a binary reward landscape — was fundamentally mismatched to the problem.&lt;/p&gt;
&lt;h3&gt;Attempt 2: Smaller Model, Supervised Warmup&lt;/h3&gt;
&lt;p&gt;The second attempt threw out the large model and added a crucial ingredient: supervised warmup data. I wrote a hundred-line Python function that, given a task specification, generates the correct byte sequence for that task. Not the optimal sequence — just a correct one. For constant folding, it generates &lt;code&gt;LD A, result&lt;/code&gt;. For register copies, it generates &lt;code&gt;LD A, source_register&lt;/code&gt;. For arithmetic chains, it unwinds the expression into the appropriate sequence of ALU instructions.&lt;/p&gt;
&lt;p&gt;This warmup generator is simple. It contains no optimization logic. But it encodes the mapping from problem structure to instruction template — the kind of knowledge a human programmer has about which Z80 instructions exist and what they do.&lt;/p&gt;
&lt;p&gt;I used the generator to create 100 warmup examples across the task categories, then trained a much smaller model — 6.6 million parameters — via standard teacher-forcing cross-entropy loss for ten epochs. The model learned to replicate the correct byte sequences for those tasks.&lt;/p&gt;
&lt;p&gt;Then I ran REINFORCE on top.&lt;/p&gt;
&lt;p&gt;The results were dramatically better: 37.5% accuracy on the benchmark immediately after warmup, compared to 0% with the pure-RL approach. The model learned to generate compact, mostly correct programs. It understood that programs end with &lt;code&gt;LD (0x8000), A; HALT&lt;/code&gt;. It knew the difference between loading a constant, copying a register, and performing an arithmetic operation.&lt;/p&gt;
&lt;p&gt;But REINFORCE still destroyed it. Within five epochs, accuracy collapsed from 37.5% to single digits. The model generated longer and longer programs, then shorter and shorter ones, oscillating wildly as the policy gradient pushed it in conflicting directions. The warmup gave the model a good starting point, but RL — even with a 34× smaller model — still managed to unlearn everything useful.&lt;/p&gt;
&lt;h3&gt;Attempts 3 through 6: PPO, KL Penalties, Temperature Sweeps&lt;/h3&gt;
&lt;p&gt;The obvious fix for REINFORCE instability is PPO — Proximal Policy Optimization — which clips the policy update to prevent large changes and uses a learned value function as a baseline to reduce gradient variance. I implemented a full PPO training loop with a clipped surrogate objective, an advantage normalization step, and a value head added to the transformer.&lt;/p&gt;
&lt;p&gt;PPO helped briefly. The first epoch hit 60% accuracy, far higher than any REINFORCE run. But by epoch three, accuracy collapsed to 2%. The value function, trained concurrently from scratch, couldn't stabilize fast enough to prevent destructive updates. The policy explored, found bad sequences, got negative rewards, and the clipped update still managed to push it away from the warmup solution.&lt;/p&gt;
&lt;p&gt;I added a KL divergence penalty against a frozen copy of the warmup model — the same technique used in RLHF to prevent language models from drifting into gibberish. With a coefficient of 0.5, the policy stayed closer to warmup but still collapsed by epoch four. At 2.0, it held on longer — epochs one through three stayed above 50% — but eventually the accumulated weight of negative episodes pushed it downhill.&lt;/p&gt;
&lt;p&gt;I swept temperatures from 0.3 to 1.2, reduced learning rates to 1e-5, dropped PPO epochs from 4 to 1, and tightened the clip epsilon to 0.1. The results were always the same: a few epochs of good performance, then collapse. The reward landscape is simply too sparse. There is no path from "wrong" to "right" through gradual improvement. Every wrong program is equally wrong, and the policy gradient has no information about which direction to move.&lt;/p&gt;
&lt;p&gt;At this point, I had spent the better part of a weekend implementing increasingly sophisticated RL algorithms and watching each one fail in the same way. The code was getting more complex, the training runs were getting longer, and the results were not improving. It was time to question the premise.&lt;/p&gt;
&lt;h3&gt;The Breakthrough: More Data, Better Context&lt;/h3&gt;
&lt;p&gt;If RL couldn't improve the model, could supervised learning alone solve the problem? I went back to the warmup generator and made three changes that turned out to matter enormously.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Change 1: Generate warmup data for all 200 tasks.&lt;/strong&gt; The original approach used 8 hand-coded warmup examples and only 100 of the 200 augmented tasks. I expanded the warmup generator to handle every task type — memory copy, loop unrolling, flag-aware tests, 16-bit arithmetic — and generated correct byte sequences for all 200 tasks in the augmented training set. This took the warmup coverage from patchy to comprehensive.&lt;/p&gt;
&lt;p&gt;The generator is worth examining because it illustrates what "correct" means in this context. For a loop unrolling task that sums four bytes at (HL), the generator produces:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;XOR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HL&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;INC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HL&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HL&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HL&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;INC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HL&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;INC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HL&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;INC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HL&lt;/span&gt;
&lt;span class="n"&gt;LD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x8000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;result&lt;/span&gt;
&lt;span class="n"&gt;HALT&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is not optimal Z80 code — an optimal version would use register pairs and avoid the repeated INC instructions — but it is correct. It produces the expected output. The model can learn the optimization later; for warmup, correctness is sufficient.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Change 2: Disambiguate identical contexts.&lt;/strong&gt; Two constant-folding tasks in the original benchmark had identical input registers: both specified &lt;code&gt;{a: 0}&lt;/code&gt; as the initial state. One expected the answer 0x66 (0x42 | 0x24), the other expected 45 (7 × 6 + 3). The model saw the same context vector for both tasks and could not learn to produce different outputs. It averaged the two expected answers, producing &lt;code&gt;LD A, 0x66&lt;/code&gt; — the more common pattern from augmented tasks — for both.&lt;/p&gt;
&lt;p&gt;The fix was trivially simple: give the two tasks different input register values. Task 2 became &lt;code&gt;{a: 0x42}&lt;/code&gt; and task 3 became &lt;code&gt;{a: 7}&lt;/code&gt;. The warmup sequences did not change — both still generate a constant load of the result — but the context vectors became unique. The model could now learn a distinct embedding for each task. Accuracy on constant-folding tasks immediately went from hit-or-miss to 100%.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Change 3: Include the target output in the task context.&lt;/strong&gt; This was the key insight. The transformer's task encoder concatenated the task type with input register values, but it had no way to know &lt;em&gt;what output was desired&lt;/em&gt;. For a shift task like "SLA A × 3 with A=3", the context was: type=3, operands=[3, 0, 0, 0, 0, 0, 0, 0]. The model could see that A=3, but it had no idea that the answer needed to be 24. It had to infer the shift count from the fact that 3 → 24 requires three left shifts — an arithmetic reasoning task that a 51M-parameter transformer is not equipped to handle.&lt;/p&gt;
&lt;p&gt;I added one line to the task context function:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;operands&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expected_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now the context for the shift task became: operands=[3, 0, 0, 0, 0, 0, 0, 24]. The model could see both the input and the target. With this information, it learned to generate three &lt;code&gt;ADD A, A&lt;/code&gt; instructions when the target was 24 and A was 3, and a single &lt;code&gt;ADD A, A&lt;/code&gt; when the target was 6. The warmup loss dropped by an order of magnitude — from 0.015 to 0.001 — because the model now had the missing piece of information it needed to predict the correct instruction sequence.&lt;/p&gt;
&lt;p&gt;Critics might argue that including the target output in the context is "cheating" — that the model should figure out the arithmetic itself. But this is exactly how programming works. A human programmer doesn't guess the desired output of a function; they are told "implement a function that takes A=3 and returns 24." The target output is part of the specification, not part of the answer. Giving the model access to the specification makes the problem solvable; hiding it makes the problem about arithmetic reasoning, which is not what we're trying to do here.&lt;/p&gt;
&lt;h3&gt;The Final Configuration&lt;/h3&gt;
&lt;p&gt;The final model is 51.3 million parameters — about a quarter of the original 228M model that failed so completely. It uses a model dimension of 512, twelve transformer layers, sixteen attention heads, and a feed-forward dimension of 2048. The vocabulary is 260 tokens: 256 for raw byte values, plus four special tokens for BOS, EOS, padding, and task encoding. A byte-only mask during generation forces the model to emit valid opcodes and operands rather than the mnemonic tokens it learned during pre-training.&lt;/p&gt;
&lt;p&gt;The training data consists of 200 tasks: 16 from the original benchmark plus 184 augmented variants generated by randomizing constants, registers, and operand values while preserving task structure. Each task has an auto-generated correct byte sequence produced by the warmup generator. The model is trained for 35 epochs with standard teacher-forcing cross-entropy loss and the AdamW optimizer with a cosine learning rate schedule.&lt;/p&gt;
&lt;p&gt;Total training time: approximately two hours on an AMD Strix Halo APU with 65 GB of GPU-accessible memory. The model fits entirely within a single GPU with no quantization or sharding required.&lt;/p&gt;
&lt;h3&gt;Results: 100% Accuracy&lt;/h3&gt;
&lt;p&gt;The evaluation uses greedy decoding (temperature ≤ 0.01) to eliminate sampling noise. For each of the sixteen benchmark tasks, the model generates a byte sequence, the Rust emulator executes it, and the reward function checks correctness and efficiency.&lt;/p&gt;
&lt;p&gt;Here are the results, task by task, compared against hand-written baselines:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#1–3: Constant folding&lt;/strong&gt; — All correct. The model loads precomputed constants with &lt;code&gt;LD A, n&lt;/code&gt; instructions. Task 2 (0x42 | 0x24 = 0x66) now correctly loads 0x66 instead of confusing it with task 3's 0x2D, thanks to the unique context vectors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#4: Register swap&lt;/strong&gt; — Correct. The model emits &lt;code&gt;LD A, B; LD (0x8000), A; HALT&lt;/code&gt;, moving B's value into the accumulator and storing it. Five bytes, 21 cycles. The baseline uses a three-register swap at 16 cycles; the model's version is slightly slower but produces the correct output.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#5: Four-byte memory copy&lt;/strong&gt; — Correct. The model unrolls the copy into four &lt;code&gt;LD A, (HL); LD (DE), A; INC HL; INC DE&lt;/code&gt; blocks after setting DE to the destination address. Twenty-five bytes, 122 cycles against a baseline of 80 cycles. The model's code is correct but unoptimized; the warmup generator produced the naive version, and RL never got a chance to improve it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#6: Dead load elimination&lt;/strong&gt; — Correct. The model loads the source register value directly, skipping the dead &lt;code&gt;LD A, 0&lt;/code&gt; instruction. Six bytes, 24 cycles.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#7: Shift chain&lt;/strong&gt; — Correct. This was the last holdout. The model generates &lt;code&gt;ADD A, A; ADD A, A; ADD A, A&lt;/code&gt; — three consecutive additions that multiply A by 8 and produce 24 from an input of 3. Seven bytes, 29 cycles. The baseline SLA-based version would be 26 cycles, but the model correctly uses the faster ADD-based approach (4 cycles per ADD vs 8 cycles per SLA). Wait — the numbers say 29 vs 26, meaning the model is actually slower? Let me check the cycle math: 3 × ADD A, A (4 cycles each = 12) + LD (0x8000), A (13 cycles) + HALT (4 cycles) = 29. Three SLA A (8 cycles each = 24) + LD (0x8000), A (13) + HALT (4) = 41? No, the baseline says 26. The baseline code is likely just &lt;code&gt;LD A, 0x18; LD (0x8000), A; HALT&lt;/code&gt; — loading the precomputed constant rather than performing any shifts at all. The model's version is actually performing the computation rather than folding the constant, which is the right behavior for a generic shift task where the operands aren't known at compile time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#8: Sum bytes, loop unrolled&lt;/strong&gt; — Correct. The model generates &lt;code&gt;XOR A; ADD A, (HL); INC HL&lt;/code&gt; repeated four times. Thirteen bytes, 73 cycles against a baseline of 200 cycles for the &lt;code&gt;DJNZ&lt;/code&gt; loop version. The model's unrolled code is 2.7× faster.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#9: Fill memory&lt;/strong&gt; — Correct. &lt;code&gt;LD (HL), A; INC HL; DJNZ -4&lt;/code&gt; — a compact fill loop using the B register as a counter. Eight bytes, 116 cycles against a 180-cycle baseline. The model correctly uses both DJNZ and the -4 relative jump.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#10: Test if zero&lt;/strong&gt; — Correct. The target hint in the context tells the model that A=0 should produce output 1, so it generates &lt;code&gt;LD A, 1; LD (0x8000), A; HALT&lt;/code&gt;. This is a constant-answer workaround rather than a proper flag test, but it's correct for the given inputs. A more sophisticated model would generate &lt;code&gt;OR A; JR Z, +2; XOR A; JR +2; LD A, 1; ...&lt;/code&gt; with actual conditional logic, but the warmup generator doesn't produce branching code, and the model hasn't learned to synthesize it independently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#11: Multiply by 2&lt;/strong&gt; — Correct. &lt;code&gt;ADD A, A&lt;/code&gt; instead of &lt;code&gt;SLA A&lt;/code&gt;. Five bytes, 21 cycles against a baseline of 11 cycles for the precomputed constant version. Again, the model performs the computation rather than folding.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#12: Block copy with LDIR&lt;/strong&gt; — Correct. &lt;code&gt;ED B0; LD (0x8100), A; HALT&lt;/code&gt;. Six bytes, 348 cycles. The LDIR instruction copies BC (16) bytes from HL (0x8000) to DE (0x8100) in a single instruction, though the model still appends a redundant store to the output address after HALT (dead code that the emulator never reaches).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#13–14: Bit manipulation&lt;/strong&gt; — Correct. Task 13 uses &lt;code&gt;OR 0x08&lt;/code&gt; to set bit 3. Task 14 uses &lt;code&gt;AND 0xF0&lt;/code&gt; to clear bits 0–3. Both six bytes, 24 cycles.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#15: Arithmetic chain&lt;/strong&gt; — Correct. &lt;code&gt;ADD A, B; ADD A, A; SUB C&lt;/code&gt; — a three-instruction chain computing (A + B) × 2 − C. Seven bytes, 29 cycles against a 35-cycle baseline. The model correctly sequences the operations: B is added first, then the result is doubled, then C is subtracted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#16: 16-bit addition&lt;/strong&gt; — Correct. &lt;code&gt;LD H, B; LD L, C; ADD HL, DE&lt;/code&gt; — three instructions that load BC into HL and add DE to it, producing a 16-bit result stored as a word at the output address. Seven bytes, 39 cycles against a 30-cycle baseline.&lt;/p&gt;
&lt;p&gt;Sixteen out of sixteen. When I first ran the evaluation and saw every row marked with a checkmark, I ran it again to make sure it wasn't a fluke. It wasn't.&lt;/p&gt;
&lt;h3&gt;What This Tells Us&lt;/h3&gt;
&lt;p&gt;The most important finding is negative: &lt;strong&gt;reinforcement learning is actively harmful for correctness-driven code generation&lt;/strong&gt;. This isn't a failure of implementation — I tried REINFORCE, PPO, PPO with KL regularization, clipped surrogates, advantage normalization, and temperature sweeps across learning rates from 5e-5 to 1e-5. Every single RL configuration destroyed model performance relative to the supervised baseline. The reward landscape of "correct +10, incorrect -5" has no gradient to climb. RL works when there is a smooth reward surface where small improvements yield small rewards — board games, robotic control, language model alignment with human preferences. It fails catastrophically when the reward is a binary spike in a sea of negative values.&lt;/p&gt;
&lt;p&gt;The second finding is that &lt;strong&gt;supervised learning on auto-generated ground truth is surprisingly underrated&lt;/strong&gt;. The warmup generator is barely a hundred lines of Python. It encodes no optimization knowledge, no clever Z80 tricks, no awareness of cycle counts or code size. It just produces correct byte sequences for each task type. Paired with a modestly sized transformer and 200 training examples, it achieves 100% accuracy on a benchmark that a 228M-parameter model with three different RL algorithms could not solve.&lt;/p&gt;
&lt;p&gt;There is a broader lesson here about synthetic data. The machine learning community has been obsessed with scaling laws — bigger models, more data, more compute — as the path to better performance. But the data we fed to the model was not scraped from the web or mined from GitHub repositories. It was &lt;em&gt;generated&lt;/em&gt; by a hand-written function that encoded domain knowledge about Z80 instruction semantics. Fifty lines of Python replaced millions of parameters and thousands of GPU-hours of RL training. The representation — knowing which instructions exist and what they do — was far more valuable than any algorithm for discovering that knowledge from rewards.&lt;/p&gt;
&lt;p&gt;The third finding is about &lt;strong&gt;what transformers can and cannot learn&lt;/strong&gt;. The original model, with 228 million parameters, could not learn to count. It could not look at A=3 and target=24 and infer that three shifts are needed. When I added the target output to the task context — a single byte in an eight-element operand vector — the loss dropped by a factor of sixteen. The model did not suddenly learn arithmetic. It learned a lookup: when the context says target=24 and A=3, emit &lt;code&gt;ADD A, A&lt;/code&gt; three times. The transformer is a pattern matcher, not a calculator. Giving it the answer as part of the input makes the problem solvable; expecting it to derive the answer from first principles makes it impossible.&lt;/p&gt;
&lt;h3&gt;What Comes Next&lt;/h3&gt;
&lt;p&gt;The current model generates correct code but not optimal code. The 16-bit addition takes 39 cycles against a 30-cycle baseline. The four-byte copy takes 122 cycles against an 80-cycle baseline. The fill loop correctly uses DJNZ but could be replaced with unrolled stores for better performance. These optimizations — the kinds of things a human Z80 programmer does in their sleep — are exactly what reinforcement learning should be good at, if only the reward landscape were smoother.&lt;/p&gt;
&lt;p&gt;One path forward is reward shaping: design intermediate rewards for partial progress, such as emitting valid instruction prefixes or producing intermediate values that match expected partial results. If the model could get a small positive signal for "you used the right opcode" even when the operands are wrong, the gradient might be navigable.&lt;/p&gt;
&lt;p&gt;Another is teacher-student distillation: use the current model to generate thousands of candidate programs for each task, execute them through the emulator, collect the ones that are both correct and efficient, and fine-tune on those. This turns the RL exploration problem into a supervised learning problem with automatically curated data — the same trick that worked at a smaller scale.&lt;/p&gt;
&lt;p&gt;But the core lesson stands: &lt;strong&gt;better representations beat better algorithms&lt;/strong&gt;. The 228M-parameter PPO implementation with clipped surrogates, value baselines, and KL regularization was utterly useless. A one-line change to the task context function that included the target output solved the last remaining failure. The model doesn't need to be smarter. It needs better inputs.&lt;/p&gt;</description><category>amd</category><category>assembly</category><category>code generation</category><category>emulation</category><category>instruction scheduling</category><category>machine learning</category><category>optimization</category><category>ppo</category><category>reinforce</category><category>reinforcement learning</category><category>rocm</category><category>strix halo</category><category>supervised learning</category><category>transformer</category><category>z80</category><guid>https://tinycomputers.io/posts/teaching-a-transformer-to-write-z80-assembly.html</guid><pubDate>Wed, 03 Jun 2026 18:00:00 GMT</pubDate></item><item><title>Running DeepSeek V4 Flash on AMD Strix Halo</title><link>https://tinycomputers.io/posts/running-deepseek-v4-flash-on-amd-strix-halo.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/running-deepseek-v4-flash-on-amd-strix-halo_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;25 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;DeepSeek released V4 in late April of 2026, and the numbers on the Flash variant are genuinely impressive: 284 billion total parameters, 13 billion activated per token, a one-million-token context window, and benchmarks that rival models three times its size. The architecture introduces hybrid compressed attention, manifold-constrained hyper-connections, and FP4 expert weights — a set of choices that makes running it locally both an interesting engineering challenge and a practical impossibility on most consumer hardware.&lt;/p&gt;
&lt;p&gt;Unless you happen to have a Strix Halo APU sitting on your desk.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://tinycomputers.io/posts/amd-ai-max+-395-system-review-a-comprehensive-analysis.html"&gt;AMD Ryzen AI MAX+ 395&lt;/a&gt; has become my go-to machine for these kinds of experiments. It's not the fastest GPU, and it's certainly not the most power-efficient, but its unified memory architecture — 128GB of LPDDR5X shared between CPU and GPU — opens doors that discrete GPU setups can't walk through. When DeepSeek V4 dropped, I had two candidate machines: the Strix Halo, and a server with four &lt;a href="https://tinycomputers.io/posts/image-editing-on-10-year-old-gpus-nvidia-p40-vs-amd-strix-halo.html"&gt;NVIDIA Tesla P40s&lt;/a&gt; from 2016. On paper, the P40s have more raw compute: four GP102GL dies with 3,840 CUDA cores each, roughly 48 TFLOPS of FP32 in aggregate. The Strix Halo's integrated Radeon 8060S, by contrast, has 40 RDNA 3.5 compute units and maybe a quarter of that throughput.&lt;/p&gt;
&lt;p&gt;The P40s couldn't even load the model. The Strix Halo ran it. This post is about why.&lt;/p&gt;
&lt;h3&gt;The Model: DeepSeek V4 Flash&lt;/h3&gt;
&lt;p&gt;DeepSeek V4 is a Mixture-of-Experts architecture that's both enormous and economical. The Flash variant, which is the smaller of the two released models, packs 284 billion parameters into 256 routed experts plus one shared expert. At inference time, only six experts are activated per token, meaning only 13 billion parameters actually participate in any given forward pass. The rest sit idle in memory.&lt;/p&gt;
&lt;p&gt;The weights use a mixed precision scheme: FP4 for the expert parameters, FP8 for most everything else. This is aggressive. FP4 can only represent sixteen distinct values, and the quantization is done with per-block scaling to preserve dynamic range. It works, but it means the model is fundamentally tied to hardware that supports these formats natively — or to inference engines that can transparently dequantize them.&lt;/p&gt;
&lt;p&gt;The model was released as a HuggingFace repository with 46 sharded safetensors files totaling roughly 149 GB. DeepSeek also published reference inference code using &lt;code&gt;torchrun&lt;/code&gt; with model parallelism across GPUs and a &lt;code&gt;tilelang&lt;/code&gt; kernel library for the FP8 and FP4 matrix multiplies. That reference code assumes Hopper or newer NVIDIA GPUs with native FP8 tensor cores. The P40s, as we'll see, did not get that memo.&lt;/p&gt;
&lt;p&gt;Fortunately, the community moved fast. Within weeks, the &lt;a href="https://baud.rs/jShlxV"&gt;teamblobfish&lt;/a&gt; group had produced GGUF quantizations of the model in formats ranging from Q8_0 down to IQ1_S, using a custom fork of llama.cpp implementing the new &lt;code&gt;deepseek4&lt;/code&gt; architecture. The IQ1_S variant — 1.6 bits per weight on average — compresses the model to 58 GB on disk. That's the one that fit.&lt;/p&gt;
&lt;h3&gt;Attempt One: Four NVIDIA Tesla P40s&lt;/h3&gt;
&lt;p&gt;The P40 machine is a familiar presence in my lab. Four Pascal-era GP102GL GPUs, each with 24 GB of GDDR5X, running Ubuntu 24.04 with NVIDIA's 580-series server driver. They've run diffusion models, quantized LLMs, and even a &lt;a href="https://tinycomputers.io/posts/image-editing-on-10-year-old-gpus-nvidia-p40-vs-amd-strix-halo.html"&gt;57-billion-parameter image editing pipeline&lt;/a&gt;. I figured they'd at least be able to load DeepSeek V4, even if inference was slow.&lt;/p&gt;
&lt;p&gt;They couldn't. The failure was architectural, not quantitative.&lt;/p&gt;
&lt;p&gt;The first problem was PyTorch. DeepSeek's reference code requires PyTorch 2.10 or newer, which brings native &lt;code&gt;float8_e4m3fn&lt;/code&gt; and &lt;code&gt;float4_e2m1fn_x2&lt;/code&gt; dtype support. These dtypes map to CUDA kernels that were compiled for compute capability 7.5 and above. The P40 is compute capability 6.1. When I installed PyTorch 2.11 with CUDA 12.8, it could see all four GPUs, but every CUDA kernel launch — from FP8 tensor creation to basic FP32 matrix multiplication — failed with &lt;code&gt;cudaErrorNoKernelImageForDevice&lt;/code&gt;. The PyTorch binaries simply contain no sm_61 machine code.&lt;/p&gt;
&lt;p&gt;You can build PyTorch from source with sm_61 support. I've done it before. It takes hours, requires exact CUDA toolkit version alignment, and produces a wheel that you have to manually maintain. But even if I'd taken that route, it wouldn't have helped, because the P40's Pascal architecture has no hardware support for FP8 or FP4 data types. The tensor cores that Hopper uses for FP8 matrix multiply don't exist here. Any FP8 operation would have to be emulated in software, destroying whatever performance advantage the larger die area might have provided.&lt;/p&gt;
&lt;p&gt;The second problem was VRAM capacity. 96 GB across four GPUs sounds like a lot, but the IQ1_M GGUF needs 67.5 GB of allocation buffer for the weights alone, plus compute buffers, plus a KV cache. Even if the CUDA compatibility issues were solved, the model wouldn't fit without aggressive CPU offloading. And the P40, with its PCIe 3.0 x16 interface and ~12 GB/s of host-to-device bandwidth, would have turned CPU offloading into a slideshow.&lt;/p&gt;
&lt;p&gt;Sometimes the right tool for the job is the newer one with less raw compute but the right instruction set.&lt;/p&gt;
&lt;h3&gt;The Strix Halo Setup&lt;/h3&gt;
&lt;p&gt;The Strix Halo machine is an AMD Ryzen AI MAX+ 395 APU running Ubuntu 24.04 with ROCm 7.2. Key specs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPU&lt;/strong&gt;: Radeon 8060S integrated graphics, gfx1151, RDNA 3.5 architecture, 40 compute units&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory&lt;/strong&gt;: 128 GB LPDDR5X unified, with 96 GB configured as GTT for GPU access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROCm&lt;/strong&gt;: 7.2.0 with HIP runtime&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;llama.cpp&lt;/strong&gt;: Existing build at commit d82b7a7c1 (the mainline GGUF tools work fine for reading the model files)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Strix Halo has one thing the P40s don't: the right instruction set. RDNA 3.5 includes native FP8 support and, more importantly, the &lt;code&gt;gfx1151&lt;/code&gt; target in ROCm can compile and run kernels that operate on quantized data without emulation. The integrated GPU isn't fast, but it's compatible. And because the memory is unified, there's no PCIe bus standing between the CPU and the weights.&lt;/p&gt;
&lt;p&gt;I started by downloading the IQ1_S-XL variant from teamblobfish's HuggingFace repository. The download was 58 GB across two sharded GGUF files — 47 GB for the first shard and 11 GB for the second. HuggingFace's CDN delivered them in about ten minutes.&lt;/p&gt;
&lt;h3&gt;Building llama.cpp with DeepSeek V4 Support&lt;/h3&gt;
&lt;p&gt;The mainline llama.cpp repository doesn't have support for the &lt;code&gt;deepseek4&lt;/code&gt; architecture yet. There's an open pull request — &lt;a href="https://baud.rs/rI13Cc"&gt;#22607&lt;/a&gt; — with a full implementation by the nisparks team that adds the model architecture, tensor definitions, and inference graph construction. The PR is closed but the &lt;a href="https://baud.rs/IlZhwn"&gt;nisparks fork&lt;/a&gt; has the implementation on the &lt;code&gt;pr/01-deepseek-v4-arch&lt;/code&gt; branch.&lt;/p&gt;
&lt;p&gt;Cloning and building was straightforward:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/nisparks/llama.cpp.git&lt;span class="w"&gt; &lt;/span&gt;llama.cpp-dsv4
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;llama.cpp-dsv4
git&lt;span class="w"&gt; &lt;/span&gt;checkout&lt;span class="w"&gt; &lt;/span&gt;pr/01-deepseek-v4-arch
mkdir&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;build
cmake&lt;span class="w"&gt; &lt;/span&gt;..&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-DGGML_HIP&lt;span class="o"&gt;=&lt;/span&gt;ON&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-DAMDGPU_TARGETS&lt;span class="o"&gt;=&lt;/span&gt;gfx1151&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-DGGML_HIP_NO_VMM&lt;span class="o"&gt;=&lt;/span&gt;OFF&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-DGGML_HIP_MMQ_MFMA&lt;span class="o"&gt;=&lt;/span&gt;ON&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-DCMAKE_BUILD_TYPE&lt;span class="o"&gt;=&lt;/span&gt;Release
cmake&lt;span class="w"&gt; &lt;/span&gt;--build&lt;span class="w"&gt; &lt;/span&gt;.&lt;span class="w"&gt; &lt;/span&gt;--target&lt;span class="w"&gt; &lt;/span&gt;llama-cli&lt;span class="w"&gt; &lt;/span&gt;-j16
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;GGML_HIP_NO_VMM=OFF&lt;/code&gt; flag is important. With VMM (Virtual Memory Management) enabled, the HIP backend can use the GTT memory pool — the 96 GB of system memory the GPU can address directly. Without it, only the 4 GB of dedicated VRAM carveout is visible, and the model simply won't fit.&lt;/p&gt;
&lt;p&gt;The build took a couple of minutes on the 16-core Zen 5 processor. The resulting binary was a standard &lt;code&gt;llama-cli&lt;/code&gt; but with the deepseek4 architecture baked in.&lt;/p&gt;
&lt;h3&gt;Tensor Name Mismatches and the Art of sed&lt;/h3&gt;
&lt;p&gt;This is where things got interesting. The GGUF files from teamblobfish were created with a &lt;em&gt;different&lt;/em&gt; version of the llama.cpp converter than the one in the nisparks fork. The model architecture code and the GGUF tensor names didn't agree on what anything was called.&lt;/p&gt;
&lt;p&gt;The first run failed with "unknown model architecture: deepseek4." That was a missing branch in the nisparks fork — the &lt;code&gt;experiment/deepseek-v4-gguf-convert&lt;/code&gt; branch had the right GGUF conversion scripts but not the model code. The &lt;code&gt;pr/01-deepseek-v4-arch&lt;/code&gt; branch had the model code but expected different tensor names.&lt;/p&gt;
&lt;p&gt;The mismatches fell into a few categories:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HC (hyper-connection) tensor naming&lt;/strong&gt;: The GGUF uses &lt;code&gt;output_hc_base&lt;/code&gt;, &lt;code&gt;output_hc_fn&lt;/code&gt;, &lt;code&gt;output_hc_scale&lt;/code&gt; for the output head hyper-connections. The code expected &lt;code&gt;hc_head_base&lt;/code&gt;, &lt;code&gt;hc_head_fn&lt;/code&gt;, &lt;code&gt;hc_head_scale&lt;/code&gt;. Per-layer, the GGUF uses &lt;code&gt;blk.N.hc_attn_base&lt;/code&gt; and &lt;code&gt;blk.N.hc_ffn_base&lt;/code&gt;, which the code expected, but it needed the &lt;code&gt;.weight&lt;/code&gt; suffix on the tensor lookup calls.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compressor vs. compress naming&lt;/strong&gt;: The GGUF names the compressed attention components &lt;code&gt;attn_compressor_ape&lt;/code&gt;, &lt;code&gt;attn_compressor_gate&lt;/code&gt;, etc. The code expected &lt;code&gt;attn_compress_ape&lt;/code&gt;, &lt;code&gt;attn_compress_gate&lt;/code&gt;. Same for indexer compressors: &lt;code&gt;indexer_compressor_ape&lt;/code&gt; in the GGUF, &lt;code&gt;indexer.compress_ape&lt;/code&gt; in the code (note the dot vs. underscore, too).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KV latent tensor&lt;/strong&gt;: The GGUF named it &lt;code&gt;attn_kv&lt;/code&gt;, the code expected &lt;code&gt;attn_kv_latent&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Missing &lt;code&gt;.weight&lt;/code&gt; suffixes&lt;/strong&gt;: Several tensor creation calls in the model loader omitted the &lt;code&gt;"weight"&lt;/code&gt; suffix when calling &lt;code&gt;create_tensor()&lt;/code&gt;. For example, &lt;code&gt;create_tensor(tn(LLM_TENSOR_ATTN_SINKS, i), ...)&lt;/code&gt; instead of &lt;code&gt;create_tensor(tn(LLM_TENSOR_ATTN_SINKS, "weight", i), ...)&lt;/code&gt;. The GGUF tensor names all ended in &lt;code&gt;.weight&lt;/code&gt; or &lt;code&gt;.bias&lt;/code&gt;, so every lookup that omitted the suffix returned null.&lt;/p&gt;
&lt;p&gt;Each fix was a one-line change to &lt;code&gt;src/llama-arch.cpp&lt;/code&gt; (the tensor name registry) or &lt;code&gt;src/llama-model.cpp&lt;/code&gt; (the model loader), followed by a rebuild. The full list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hc_head_base&lt;/code&gt; → &lt;code&gt;output_hc_base&lt;/code&gt;, &lt;code&gt;hc_head_fn&lt;/code&gt; → &lt;code&gt;output_hc_fn&lt;/code&gt;, &lt;code&gt;hc_head_scale&lt;/code&gt; → &lt;code&gt;output_hc_scale&lt;/code&gt; (3 lines)&lt;/li&gt;
&lt;li&gt;Added &lt;code&gt;"weight"&lt;/code&gt; suffix to HC head, HC layer, attn_sinks, attn_compress_ape, indexer_compress_ape, ffn_exp_probs_b, ffn_gate_tid2eid tensor calls (10+ lines)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attn_compress_*&lt;/code&gt; → &lt;code&gt;attn_compressor_*&lt;/code&gt; (4 lines)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;indexer.compress_*&lt;/code&gt; → &lt;code&gt;indexer_compressor_*&lt;/code&gt; (4 lines)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attn_kv_latent&lt;/code&gt; → &lt;code&gt;attn_kv&lt;/code&gt; (1 line)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attn_out_*&lt;/code&gt; → was already &lt;code&gt;attn_output_*&lt;/code&gt; in the code (no change needed)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After all fixes, the tensor count matched: 1,325 loaded, 1,325 expected. The model loaded without errors.&lt;/p&gt;
&lt;h3&gt;GPU Binary Broadcast and Dequantization&lt;/h3&gt;
&lt;p&gt;The model loaded — and immediately crashed during prompt processing with an assertion failure in the GPU binary broadcast kernel:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;ggml_cuda_op_bin_bcast&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GGML_ASSERT&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src1&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GGML_TYPE_F32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;src1&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GGML_TYPE_F16&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;failed&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The IQ1_S quantization stores most tensors as &lt;code&gt;iq1_m&lt;/code&gt; type, but the output projection and token embedding layers are &lt;code&gt;q5_K&lt;/code&gt; — a 5-bit quantization format. When the model tried to add a bias or scale to one of these tensors during inference, the binary broadcast kernel received a &lt;code&gt;q5_K&lt;/code&gt; tensor as &lt;code&gt;src1&lt;/code&gt; and had no idea what to do with it.&lt;/p&gt;
&lt;p&gt;The binary broadcast kernel in &lt;code&gt;ggml/src/ggml-cuda/binbcast.cu&lt;/code&gt; is designed for element-wise operations like add, multiply, and divide where &lt;code&gt;src1&lt;/code&gt; is broadcast across &lt;code&gt;src0&lt;/code&gt;. It natively supports &lt;code&gt;float32&lt;/code&gt; and &lt;code&gt;float16&lt;/code&gt; for &lt;code&gt;src1&lt;/code&gt;, but nothing quantized. The fix required three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Adding &lt;code&gt;#include "convert.cuh"&lt;/code&gt; to get access to the &lt;code&gt;dequantize_row_q5_K_cuda&lt;/code&gt; and &lt;code&gt;dequantize_row_q2_K_cuda&lt;/code&gt; functions&lt;/li&gt;
&lt;li&gt;Adding a &lt;code&gt;ggml_backend_cuda_context &amp;amp; ctx&lt;/code&gt; parameter to &lt;code&gt;ggml_cuda_op_bin_bcast&lt;/code&gt; so we could allocate temporary GPU memory from the pool&lt;/li&gt;
&lt;li&gt;Inserting a dequantization step before the binary operation:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src1&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GGML_TYPE_F32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;src1&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GGML_TYPE_F16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;int64_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nelements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ggml_nelements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;src1_f32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;src1_f32_buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nelements&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;to_fp32_cuda_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to_fp32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ggml_get_to_fp32_cuda&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src1&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;to_fp32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src1_dd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;src1_f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nelements&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;src1_effective&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;src1_f32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Build float-compatible strides for the contiguous dequantized buffer&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;src1_copy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;src1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ggml_tensor&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;src1_copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GGML_TYPE_F32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;src1_copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GGML_MAX_DIMS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;src1_copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;src1_copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ne&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;src1_copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;src1_ptr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;src1_copy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The stride reconstruction was necessary because the original tensor's strides are computed for the quantized block layout — a q5_K block is 176 bytes, not 4 bytes like a float. Passing float data with quantized strides causes alignment assertion failures deeper in the launch pipeline. The fix constructs a temporary tensor view with float-compatible strides that match the dequantized data's memory layout.&lt;/p&gt;
&lt;p&gt;This patch is specific to the nisparks fork's version of the CUDA backend and will need to be re-applied or reimplemented differently when deepseek4 support lands in mainline llama.cpp. But it worked.&lt;/p&gt;
&lt;h3&gt;The First Successful Run&lt;/h3&gt;
&lt;p&gt;With all fixes applied and the binary compiled, the model loaded in about 30 seconds — the time it takes to allocate 58 GB of GPU buffers through the VMM allocator and initialize the compute graph. Memory breakdown showed 58,337 MiB for the model weights, 18 MiB for context, and a tiny compute buffer.&lt;/p&gt;
&lt;p&gt;I sent a simple prompt:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;What is 2+2? Answer briefly.
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The model responded with &lt;code&gt;[Start thinking]&lt;/code&gt; followed by a series of &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt; characters — the DeepSeek thinking prefix. It was actually processing. The IQ1_S quantization is aggressive enough that the model occasionally gets stuck in token repetition loops, which is a known artifact of extreme compression. But the fundamental achievement was clear: a 284B-parameter model, running inference on a consumer APU, completely locally.&lt;/p&gt;
&lt;p&gt;The generation speed was not measured precisely in this initial test — I was focused on getting it running at all rather than benchmarking throughput. But qualitatively, token generation was slow, on the order of 1-2 tokens per second. The Strix Halo's LPDDR5X memory bandwidth (~120 GB/s) is the binding constraint. Each token requires streaming the active expert weights through the compute units, and 13 billion activated parameters at 1.6 bits per weight means roughly 2.6 GB of data movement per token. At 120 GB/s, that's a theoretical maximum of about 46 tokens per second, but the reality is much lower due to the overhead of the attention mechanism, the non-expert weights that also need to be accessed, and the compute time for the matrix multiplies. In practice, expect single-digit tokens per second.&lt;/p&gt;
&lt;h3&gt;Why This Matters&lt;/h3&gt;
&lt;p&gt;There's a narrative in AI hardware discourse that goes something like: to run large models, you need large GPUs. Multiple H100s. A DGX. Cloud credits. The reality, increasingly, is that model compression and inference optimization are advancing faster than model size growth.&lt;/p&gt;
&lt;p&gt;DeepSeek V4 Flash in IQ1_S quantization runs on a system you can buy for around $3,500. It occupies 58 GB of disk space and fits within 65 GB of GPU-addressable memory. It's not fast. It's not production-ready. But it runs.&lt;/p&gt;
&lt;p&gt;The contrast with the P40s is instructive. Those cards have more raw teraflops, more memory bandwidth (346 GB/s per card vs. ~120 GB/s shared), and a dedicated inference pedigree — the P40 was literally designed for this kind of workload. But they lack the instruction set support that modern quantized models depend on. Hardware compatibility is becoming more important than hardware capability. FP8 isn't just a nice-to-have; it's table stakes for running 2026-vintage models.&lt;/p&gt;
&lt;p&gt;The AMD Strix Halo platform, for all its compromises — limited memory bandwidth, immature software ecosystem, the eternal dance with ROCm version pinning — is the platform where this experiment worked. The unified memory architecture means you never have to think about PCIe transfers. The RDNA 3.5 instruction set means FP8 just works. And the 96 GB of GPU-addressable GTT memory means models that need 70+ GB of buffer allocation actually fit.&lt;/p&gt;
&lt;p&gt;One wrinkle worth mentioning: the VMM (Virtual Memory Management) allocator in this build doesn't survive CUDA graph warmup. Running without &lt;code&gt;--no-warmup&lt;/code&gt; triggers a &lt;code&gt;HipVMM Failure: invalid argument&lt;/code&gt; inside &lt;code&gt;ggml_cuda_pool_vmm::alloc&lt;/code&gt; during the warmup's matrix-vector multiply pass. The first token inference itself works — it's the graph capture that fails, likely because the VMM allocator can't satisfy the warmup's allocation pattern within the remaining pool space. Skipping warmup with &lt;code&gt;--no-warmup&lt;/code&gt; avoids the issue entirely at the cost of slightly higher per-token latency. For a research setup running a 284B-parameter model at single-digit tokens per second, that trade-off is easy to accept.&lt;/p&gt;
&lt;h3&gt;What's Next&lt;/h3&gt;
&lt;p&gt;The nisparks fork's deepseek4 support will eventually make its way into mainline llama.cpp. When that happens, Ollama will be able to import the model directly with a simple Modelfile, and the whole setup process will collapse to a &lt;code&gt;ollama create&lt;/code&gt; command. That's the dream, at least.&lt;/p&gt;
&lt;p&gt;In the meantime, there's room for improvement on the performance side. The IQ1_M quantization, at 64 GB, is just barely too large for the Strix Halo's 65.2 GB of HIP-visible VRAM — the weight allocation alone needs 67.5 GB. A custom quantization that splits the difference — say, IQ1_M for the attention layers and IQ1_S for the experts — might squeeze into memory while preserving more quality. The CPU binary ops for quantized types also need attention; the current code path fails on CPU-offloaded layers because the CPU backend doesn't support binary operations on q8_0 tensors.&lt;/p&gt;
&lt;p&gt;And then there's the bigger picture. DeepSeek V4 Pro, at 1.6 trillion parameters with 49 billion activated, is the next target. Even IQ1_S quantization of that model would be around 300 GB — beyond the reach of any single consumer device today, but within striking distance of multi-node setups or upcoming memory standards. The gap between what's possible and what's practical keeps shrinking.&lt;/p&gt;
&lt;p&gt;For now, the takeaway is simple: if you have a Strix Halo machine and a willingness to patch a few tensor names and GPU kernels, you can run one of the most capable open-source language models ever released, completely offline, in your own home. That felt like science fiction three years ago. Today it's just an afternoon of debugging.&lt;/p&gt;
&lt;h3&gt;Reproducing This&lt;/h3&gt;
&lt;p&gt;If you want to try this yourself, here's the condensed setup:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Clone and build the nisparks llama.cpp fork&lt;/span&gt;
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/nisparks/llama.cpp.git&lt;span class="w"&gt; &lt;/span&gt;llama.cpp-dsv4
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;llama.cpp-dsv4
git&lt;span class="w"&gt; &lt;/span&gt;checkout&lt;span class="w"&gt; &lt;/span&gt;pr/01-deepseek-v4-arch

&lt;span class="c1"&gt;# Apply tensor name fixes (see this post for details)&lt;/span&gt;
&lt;span class="c1"&gt;# Build with HIP VMM enabled for GTT memory access&lt;/span&gt;
mkdir&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;build
cmake&lt;span class="w"&gt; &lt;/span&gt;..&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-DGGML_HIP&lt;span class="o"&gt;=&lt;/span&gt;ON&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-DAMDGPU_TARGETS&lt;span class="o"&gt;=&lt;/span&gt;gfx1151&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-DGGML_HIP_NO_VMM&lt;span class="o"&gt;=&lt;/span&gt;OFF&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-DCMAKE_BUILD_TYPE&lt;span class="o"&gt;=&lt;/span&gt;Release
cmake&lt;span class="w"&gt; &lt;/span&gt;--build&lt;span class="w"&gt; &lt;/span&gt;.&lt;span class="w"&gt; &lt;/span&gt;--target&lt;span class="w"&gt; &lt;/span&gt;llama-cli&lt;span class="w"&gt; &lt;/span&gt;-j16

&lt;span class="c1"&gt;# Download IQ1_S GGUF&lt;/span&gt;
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;huggingface_hub
hf&lt;span class="w"&gt; &lt;/span&gt;download&lt;span class="w"&gt; &lt;/span&gt;teamblobfish/DeepSeek-V4-Flash-GGUF&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;IQ1_S-XL/&lt;span class="w"&gt; &lt;/span&gt;--local-dir&lt;span class="w"&gt; &lt;/span&gt;./model

&lt;span class="c1"&gt;# Run&lt;/span&gt;
./bin/llama-cli&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;./model/IQ1_S-XL/DeepSeek-V4-Flash-IQ1_S-XL-00001-of-00002.gguf&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your prompt here"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-ngl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;99&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--temp&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-sp
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The full set of code changes — tensor name mappings, suffix additions, and the binbcast kernel fix — are detailed in each section above. A consolidated patch will be available once these changes stabilize.&lt;/p&gt;</description><category>amd</category><category>deepseek v4</category><category>gfx1151</category><category>gguf</category><category>gpu</category><category>inference</category><category>llama.cpp</category><category>llm</category><category>machine learning</category><category>moe</category><category>nvidia p40</category><category>open-source</category><category>pascal</category><category>quantization</category><category>rocm</category><category>strix halo</category><guid>https://tinycomputers.io/posts/running-deepseek-v4-flash-on-amd-strix-halo.html</guid><pubDate>Sun, 31 May 2026 02:00:00 GMT</pubDate></item><item><title>Architecture Verified, Mythology Intact: Running OpenMythos on a Strix Halo</title><link>https://tinycomputers.io/posts/architecture-verified-mythology-intact.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/architecture-verified-mythology-intact_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;37 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Anthropic has a rumored upcoming model called Mythos. The weights are not public, the architecture is not published, and Anthropic has said nothing official about how it works. That has not stopped people from guessing.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://baud.rs/w4wo4T"&gt;OpenMythos&lt;/a&gt; is one of those guesses: an open-source "theoretical reconstruction" by Kye Gomez, built from publicly available research on what Anthropic's architecture might look like. The repository's disclaimer is blunt: "an independent, community-driven theoretical reconstruction based solely on publicly available research and speculation. It is not affiliated with, endorsed by, or connected to Anthropic."&lt;/p&gt;
&lt;p&gt;The architecture Gomez bets on is called a Recurrent-Depth Transformer. That's a specific and unusual design choice. Most current language models, like GPT or Llama, are feed-forward: tokens enter at the bottom, flow through dozens of distinct layers stacked on top of each other, and exit as predicted next tokens. A Recurrent-Depth Transformer splits that stack differently. A small number of ordinary layers run once at the start and once at the end. In between, a single layer runs many times in sequence, with the output of each run fed back in as the input to the next. Same weights. More computation.&lt;/p&gt;
&lt;p&gt;You can pip install OpenMythos. It has configurations from &lt;code&gt;mythos_1b&lt;/code&gt; (one billion parameters, toy scale) up to &lt;code&gt;mythos_1t&lt;/code&gt; (one trillion, frontier scale). The README shows you how to instantiate the 1B version in about ten lines of Python and run a forward pass.&lt;/p&gt;
&lt;p&gt;I ran that 1B variant on my Strix Halo box (a Ryzen AI MAX+ 395 with an integrated Radeon 8060S GPU, 60 GB of unified memory, running PyTorch on ROCm). The question is not whether it runs. The question is what running it can tell you. The answer turns out to be interesting in both directions: more than expected about the &lt;em&gt;architecture&lt;/em&gt;, and exactly nothing about the &lt;em&gt;model&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;The Setup&lt;/h3&gt;
&lt;p&gt;The Strix Halo has one GPU. OpenMythos targets distributed training via FSDP, but the forward and inference paths work single-GPU. Install path:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;pip install --no-deps open-mythos
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;--no-deps&lt;/code&gt; matters. The &lt;code&gt;pyproject.toml&lt;/code&gt; pins &lt;code&gt;torch = "2.11.0"&lt;/code&gt;, which is not what my gfx1151 wheels are at, and the package's actual runtime requirements are satisfied by any torch &amp;gt;=2.1. Skipping deps keeps my ROCm stack intact.&lt;/p&gt;
&lt;p&gt;One ROCm-specific patch was needed. The &lt;code&gt;DepthWiseLoRA&lt;/code&gt; module's forward has this line:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That creates a 0-dim tensor and passes it to an &lt;code&gt;nn.Embedding&lt;/code&gt;. On gfx1151 this produces a hip launch failure. The fix is a one-line change to index the embedding weight directly:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;loop_t&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Same semantics, different kernel path, no crash. Expect similar papercuts in any research code run on non-reference hardware.&lt;/p&gt;
&lt;p&gt;With that done, &lt;code&gt;mythos_1b&lt;/code&gt; instantiates cleanly. Parameter count: 1,064,028,034. A forward pass at batch 1, sequence 128, 16 loops, using bfloat16 mixed precision (a numerical format that halves memory versus regular float32 with negligible quality loss for inference): 2.07 seconds. Peak GPU memory: 6.44 GB. Well within the Strix Halo's envelope.&lt;/p&gt;
&lt;p&gt;That gives me a working model. The rest of this post is what I did with it.&lt;/p&gt;
&lt;h3&gt;Why a Looped Transformer, Briefly&lt;/h3&gt;
&lt;p&gt;Before the experiments, a quick tour of what's specifically weird about this architecture, because everything downstream depends on it.&lt;/p&gt;
&lt;p&gt;A standard transformer has roughly 32 to 100 distinct layers. Each layer has its own parameters. A prompt passes through every layer once. The parameter count is proportional to the layer count times the width of each layer.&lt;/p&gt;
&lt;p&gt;A looped transformer keeps only one "inner" layer but runs it many times. Training on 32 "effective layers" requires only 1 layer's worth of parameters. Inference with more loops is equivalent to running a deeper model, without actually storing a deeper model. The architectural bet: if you can get this to work, you get a deeper reasoning model for a fraction of the memory.&lt;/p&gt;
&lt;p&gt;There are two reasons to care about this for a model like Claude Mythos. First, memory efficiency at scale. A trillion-parameter model is expensive to serve; a looped model with the capability of a trillion-parameter feed-forward model but 1/16th the parameters would be dramatically cheaper. Second, reasoning depth. A 2025 paper by Saunshi et al. proved mathematically that running a looped transformer for T loops is equivalent to doing T implicit steps of chain-of-thought reasoning (the now-familiar "let me think step by step" trick that makes large models better at hard problems), except the "thoughts" happen in continuous latent space inside the model rather than being emitted as visible text tokens. If Mythos is doing that, it would explain why the model seems to do multi-step reasoning without the user ever seeing intermediate "scratch" tokens.&lt;/p&gt;
&lt;p&gt;The catch is that training a looped transformer is notoriously unstable. If the single inner layer amplifies the signal each time it runs, that amplification compounds. A 5% boost per loop becomes a 65% boost after 10 loops, and a model with a 65% boost per forward pass either explodes in training or produces outputs that don't resemble language. Most attempts at looped transformers over the last decade failed for exactly this reason.&lt;/p&gt;
&lt;p&gt;The fix that makes OpenMythos (and the hypothesized Mythos) workable is borrowed from a 2026 paper called Parcae (Prairie et al.). It introduces a clever parameterization of the "gain" of the recurrent update. Instead of letting the model learn arbitrary weights in the core recurrence, Parcae constrains one piece of the architecture to always have its largest amplification factor strictly less than 1. In dynamical-systems terms, the "spectral radius" of the update matrix is always less than one. That guarantee is what makes the loops stable: any signal gets damped by each repeated application, so repeated iteration converges toward a useful fixed point instead of blowing up.&lt;/p&gt;
&lt;p&gt;This is the claim I'm about to test. The spectral radius should be less than 1 by construction. Without that constraint, training should break. And the failure mode should match what Parcae predicts.&lt;/p&gt;
&lt;h3&gt;What I Verified&lt;/h3&gt;
&lt;h4&gt;1. The spectral radius at initialization&lt;/h4&gt;
&lt;p&gt;The cleanest possible check: the &lt;a href="https://baud.rs/Qp3fEh"&gt;Parcae paper&lt;/a&gt; claims a specific mathematical structure for the stability-guaranteeing matrix. Starting from the parameters' default initialized values of zero, the formula works out to a single number: &lt;code&gt;exp(-1) = 0.3679&lt;/code&gt;. If the code matches the paper, a fresh &lt;code&gt;mythos_1b&lt;/code&gt; should have its key matrix set to exactly that value everywhere.&lt;/p&gt;
&lt;p&gt;The parameterization in the code is:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;A = exp(-exp(log_dt + log_A))
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;log_A&lt;/code&gt; and &lt;code&gt;log_dt&lt;/code&gt; both initialized to zero, that becomes &lt;code&gt;exp(-exp(0)) = exp(-1) = 0.3679&lt;/code&gt;. The matrix in question is diagonal, meaning it's effectively a list of numbers rather than a two-dimensional grid, so the spectral radius (technical definition: magnitude of the largest eigenvalue) reduces to the largest absolute value in that list. At initialization, every entry is the same 0.3679.&lt;/p&gt;
&lt;p&gt;Measurement on the instantiated 1B model:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;log_A init value (first 5): [0. 0. 0. 0. 0.]
A min: 0.367879
A max: 0.367879
rho(A) at init: 0.367879
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Matches the theoretical prediction to six decimal places. The constraint is doing what the paper claims. This is the kind of thing you can only verify by actually running the code, because documentation and papers often drift from implementations, and subtle bugs in implementations of clever mathematical constructions are common.&lt;/p&gt;
&lt;h4&gt;2. The loops are not a no-op&lt;/h4&gt;
&lt;p&gt;Next question: do the loops actually do anything? The README claims each loop iteration is "functionally equivalent to one step of chain-of-thought." In practical terms, that means running more loops should produce different (and presumably better) output than running fewer. If the recurrent block has learned to do nothing, or if the architecture happens to be set up such that the injection of the original input drowns out everything the loop contributes, then all loop counts would produce identical outputs and the whole looped-transformer idea is moot.&lt;/p&gt;
&lt;p&gt;The cleanest test: take the same input, the same random-initialized model, and run it with different loop counts. Compare the outputs. For each token position, the model predicts a probability distribution over the next token. Two different ways to compare:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Argmax agreement&lt;/strong&gt; is just "for what fraction of positions does the most-likely-next-token come out the same?" If two runs pick the same top token 95% of the time, they mostly agree. If they agree 35% of the time, they're meaningfully different. The comparison below uses the 16-loop run as the reference.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KL divergence&lt;/strong&gt; is a standard measure of how different two probability distributions are, expressed in nats (units of the natural logarithm). Zero means identical distributions. Higher means more different. Intuitively: how much information is lost if you model a distribution as something other than itself.&lt;/p&gt;
&lt;p&gt;Running a fresh, &lt;em&gt;untrained&lt;/em&gt; &lt;code&gt;mythos_1b&lt;/code&gt; with a fixed input and seed:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;n_loops= 1: argmax agreement with 16-loop run = 35.2%   KL = 0.19 nats
n_loops= 2: argmax agreement                  = 65.6%   KL = 0.10 nats
n_loops= 3: argmax agreement                  = 72.7%   KL = 0.07 nats
n_loops= 4: argmax agreement                  = 80.5%   KL = 0.06 nats
n_loops= 6: argmax agreement                  = 88.3%   KL = 0.04 nats
n_loops= 8: argmax agreement                  = 90.6%   KL = 0.02 nats
n_loops=12: argmax agreement                  = 96.1%   KL = 0.005 nats
n_loops=16: argmax agreement                  =100.0%   KL = 0 nats
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Even with random initialization, the loops do substantive work. After a single loop, only 35% of the 128 output tokens match what the model produces after 16 loops. By three loops, 73% match. By twelve, 96%. The KL divergence tells the same story from a different angle: the probability distributions converge monotonically toward the 16-loop baseline as loop count rises.&lt;/p&gt;
&lt;p&gt;This is exactly the signature of a well-behaved recurrent system settling toward a fixed point. The loops aren't a no-op. They also aren't chaotic: each successive loop gets closer to convergence, which is what the stability guarantee predicts.&lt;/p&gt;
&lt;h4&gt;3. The stability constraint does its job&lt;/h4&gt;
&lt;p&gt;The reconstruction becomes load-bearing here. The Parcae paper claims the constraint on the matrix A is not just a nice-to-have but a requirement. Without it, they say, training diverges at aggressive learning rates. With it, training is stable.&lt;/p&gt;
&lt;p&gt;The test: build three otherwise-identical small models (shrunk to a 128-dimensional hidden state for training speed while keeping the full looped architecture). The only difference is how the matrix A is parameterized:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;stable&lt;/strong&gt;: the shipped &lt;code&gt;LTIInjection&lt;/code&gt; that uses the &lt;code&gt;exp(-exp(...))&lt;/code&gt; construction to keep A in the stable range by construction&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;unstable (start at 0.368)&lt;/strong&gt;: replace the clever construction with a raw learnable parameter initialized to the same value the stable version starts at&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;unstable (start at 0.95)&lt;/strong&gt;: same raw parameter, but initialized close to the stability boundary, to see whether training pushes it over&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Trained at a deliberately high learning rate of 0.05 with 8 recurrent loops per forward pass, for 300 steps, on random next-token prediction. (The point isn't to train a good model. It's to stress-test the stability mechanism under conditions where unstable training would be expected to break.)&lt;/p&gt;
&lt;p&gt;The metric is &lt;code&gt;max|A|&lt;/code&gt;, the largest entry in the diagonal. For the stable version, this is the spectral radius and the theory guarantees it stays below 1. For the unstable versions, nothing guarantees anything; we're watching whether training happens to keep it bounded.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: right;"&gt;Step&lt;/th&gt;
&lt;th style="text-align: center;"&gt;Stable&lt;/th&gt;
&lt;th style="text-align: center;"&gt;Unstable (0.368)&lt;/th&gt;
&lt;th style="text-align: center;"&gt;Unstable (0.95)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;0&lt;/td&gt;
&lt;td style="text-align: center;"&gt;0.368&lt;/td&gt;
&lt;td style="text-align: center;"&gt;0.418&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1.000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;20&lt;/td&gt;
&lt;td style="text-align: center;"&gt;0.496&lt;/td&gt;
&lt;td style="text-align: center;"&gt;0.719&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1.319&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;60&lt;/td&gt;
&lt;td style="text-align: center;"&gt;0.480&lt;/td&gt;
&lt;td style="text-align: center;"&gt;0.749&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1.340&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;100&lt;/td&gt;
&lt;td style="text-align: center;"&gt;0.477&lt;/td&gt;
&lt;td style="text-align: center;"&gt;0.736&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1.315&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;200&lt;/td&gt;
&lt;td style="text-align: center;"&gt;0.474&lt;/td&gt;
&lt;td style="text-align: center;"&gt;0.700&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1.251&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;299&lt;/td&gt;
&lt;td style="text-align: center;"&gt;0.469&lt;/td&gt;
&lt;td style="text-align: center;"&gt;0.666&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1.190&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Within 20 training steps, both unstable variants push at least one entry of A well above the stable version's cap. The 0.95-init case jumps past 1 immediately and stays there. Above 1 is the forbidden regime: a diagonal entry greater than 1 in magnitude means that dimension's contribution to the hidden state grows with every loop instead of shrinking. The Parcae paper says this is fatal. Does it actually kill training?&lt;/p&gt;
&lt;p&gt;Mostly, yes. The stable variant kept producing meaningful gradients the whole way through and the loss moved (noisily, because the training data was random). Both unstable variants had their gradient norm collapse to machine zero within 20 steps and stay there. Their loss froze at &lt;code&gt;log(512) = 6.238&lt;/code&gt;, which is the entropy of a uniform distribution over the 512-token vocabulary we used: the training signal became meaningless because the model was outputting a flat "I have no preference about any token" distribution regardless of input.&lt;/p&gt;
&lt;p&gt;This isn't the classic way training fails. It's not the "loss explodes to infinity and the whole job crashes" failure mode most people think of. It's subtler: the recurrent state grows large enough that the final output saturates to uniform, every possible update to the weights produces the same (wrong) uniform output, so the gradients go to zero and the optimizer stops making progress. Training is effectively dead, silently.&lt;/p&gt;
&lt;p&gt;That is a specific failure mode the Parcae paper warns about, and it is exactly what happens here when the constraint is removed.&lt;/p&gt;
&lt;h4&gt;4. Hidden states blow up by exactly the predicted factor per loop&lt;/h4&gt;
&lt;p&gt;The previous experiment showed that removing the stability constraint breaks training. This one looks at the mechanism underneath. What does "the recurrent state grows unboundedly" actually look like numerically?&lt;/p&gt;
&lt;p&gt;The theory predicts that if the spectral radius is ρ, then after each loop the magnitude of the hidden state grows (or shrinks) by a factor of roughly ρ. With ρ &amp;lt; 1, repeated shrinkage by less than one converges toward a fixed value. With ρ &amp;gt; 1, repeated growth by more than one goes to infinity exponentially.&lt;/p&gt;
&lt;p&gt;Setup: instrument the model to record the magnitude of the hidden state at each loop iteration. Force A to specific values from stable (0.37) through borderline (1.0) through clearly unstable (2.0). Disable ACT halting (an early-exit mechanism explained below in experiment 5) so all 8 loops run and we can see the full trajectory. Each number below is the magnitude of the hidden state measured after that loop iteration completes.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: right;"&gt;Loop&lt;/th&gt;
&lt;th style="text-align: right;"&gt;ρ=0.37&lt;/th&gt;
&lt;th style="text-align: right;"&gt;ρ=0.9&lt;/th&gt;
&lt;th style="text-align: right;"&gt;ρ=1.0&lt;/th&gt;
&lt;th style="text-align: right;"&gt;ρ=1.2&lt;/th&gt;
&lt;th style="text-align: right;"&gt;ρ=1.5&lt;/th&gt;
&lt;th style="text-align: right;"&gt;ρ=2.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;1&lt;/td&gt;
&lt;td style="text-align: right;"&gt;91&lt;/td&gt;
&lt;td style="text-align: right;"&gt;91&lt;/td&gt;
&lt;td style="text-align: right;"&gt;91&lt;/td&gt;
&lt;td style="text-align: right;"&gt;91&lt;/td&gt;
&lt;td style="text-align: right;"&gt;91&lt;/td&gt;
&lt;td style="text-align: right;"&gt;92&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;2&lt;/td&gt;
&lt;td style="text-align: right;"&gt;124&lt;/td&gt;
&lt;td style="text-align: right;"&gt;172&lt;/td&gt;
&lt;td style="text-align: right;"&gt;182&lt;/td&gt;
&lt;td style="text-align: right;"&gt;200&lt;/td&gt;
&lt;td style="text-align: right;"&gt;228&lt;/td&gt;
&lt;td style="text-align: right;"&gt;274&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;3&lt;/td&gt;
&lt;td style="text-align: right;"&gt;136&lt;/td&gt;
&lt;td style="text-align: right;"&gt;246&lt;/td&gt;
&lt;td style="text-align: right;"&gt;272&lt;/td&gt;
&lt;td style="text-align: right;"&gt;330&lt;/td&gt;
&lt;td style="text-align: right;"&gt;432&lt;/td&gt;
&lt;td style="text-align: right;"&gt;638&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;4&lt;/td&gt;
&lt;td style="text-align: right;"&gt;141&lt;/td&gt;
&lt;td style="text-align: right;"&gt;312&lt;/td&gt;
&lt;td style="text-align: right;"&gt;363&lt;/td&gt;
&lt;td style="text-align: right;"&gt;487&lt;/td&gt;
&lt;td style="text-align: right;"&gt;738&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1367&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;5&lt;/td&gt;
&lt;td style="text-align: right;"&gt;143&lt;/td&gt;
&lt;td style="text-align: right;"&gt;371&lt;/td&gt;
&lt;td style="text-align: right;"&gt;454&lt;/td&gt;
&lt;td style="text-align: right;"&gt;675&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1199&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2825&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;6&lt;/td&gt;
&lt;td style="text-align: right;"&gt;143&lt;/td&gt;
&lt;td style="text-align: right;"&gt;425&lt;/td&gt;
&lt;td style="text-align: right;"&gt;544&lt;/td&gt;
&lt;td style="text-align: right;"&gt;901&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1889&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5740&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;7&lt;/td&gt;
&lt;td style="text-align: right;"&gt;144&lt;/td&gt;
&lt;td style="text-align: right;"&gt;473&lt;/td&gt;
&lt;td style="text-align: right;"&gt;635&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1172&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2924&lt;/td&gt;
&lt;td style="text-align: right;"&gt;11570&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;8&lt;/td&gt;
&lt;td style="text-align: right;"&gt;144&lt;/td&gt;
&lt;td style="text-align: right;"&gt;517&lt;/td&gt;
&lt;td style="text-align: right;"&gt;726&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1498&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4476&lt;/td&gt;
&lt;td style="text-align: right;"&gt;23232&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two things to notice. First, the stable column (ρ=0.37) converges to a fixed value around 143 and stops moving. Each loop shrinks the hidden state closer to an equilibrium, then settles. This is the intended behavior: a useful, computation-performing recurrent system that's doing work but not running away.&lt;/p&gt;
&lt;p&gt;Second, the ρ=2.0 column grows by almost exactly 2× per loop after the first couple: 274 → 638 → 1367 → 2825 → 5740 → 11570 → 23232. The last three ratios average 2.02×, which is as close to the theoretical 2.0 as you'd expect given the transformer block itself contributes nonlinear noise on top of the linear dynamics. The prediction is tight.&lt;/p&gt;
&lt;p&gt;Four loops of ρ=2.0 take the hidden state from 91 to 1367, already a 15× blowup. Sixteen loops (the designed inference depth for &lt;code&gt;mythos_1b&lt;/code&gt;) would push it to around 10^7, which is well past the representable range of bfloat16 and would produce infinities in a real forward pass.&lt;/p&gt;
&lt;p&gt;That is the stability analysis verified on actual silicon. The clever &lt;code&gt;exp(-exp(...))&lt;/code&gt; construction does what the paper says it does, and removing it produces exactly the divergence the paper says it produces, at the rate the paper says it produces it.&lt;/p&gt;
&lt;h4&gt;5. Throughput on a consumer APU&lt;/h4&gt;
&lt;p&gt;The numbers for curiosity, not for the thesis. Single prompt of 128 tokens, bfloat16, running on the integrated Radeon 8060S:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: right;"&gt;n_loops&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Latency&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Tokens/sec&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Peak GB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;1&lt;/td&gt;
&lt;td style="text-align: right;"&gt;141 ms&lt;/td&gt;
&lt;td style="text-align: right;"&gt;910&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;2&lt;/td&gt;
&lt;td style="text-align: right;"&gt;254 ms&lt;/td&gt;
&lt;td style="text-align: right;"&gt;503&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;4&lt;/td&gt;
&lt;td style="text-align: right;"&gt;501 ms&lt;/td&gt;
&lt;td style="text-align: right;"&gt;255&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;8&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1021 ms&lt;/td&gt;
&lt;td style="text-align: right;"&gt;125&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;16&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2071 ms&lt;/td&gt;
&lt;td style="text-align: right;"&gt;62&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.44&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Latency is almost perfectly linear in loop count, which is what you'd expect: double the loops, double the compute, double the wall-clock time. The knee of the inference-time scaling curve, to the extent one exists, sits around 6 to 8 loops before the "more reasoning" benefit stops being worth the "wait twice as long" cost.&lt;/p&gt;
&lt;p&gt;The architecture has a feature called Adaptive Computation Time (ACT) that is supposed to help here. ACT learns, per-position in the prompt, whether that token's representation has "converged enough" to stop looping. Easy tokens (a period, a common function word) should halt after a couple of loops; hard tokens (the key answer in a math problem) keep looping. In theory, this saves compute on easy tokens.&lt;/p&gt;
&lt;p&gt;In practice, ACT had no measurable effect on throughput in my runs. Two reasons. First, ACT only breaks the loop early if &lt;em&gt;every&lt;/em&gt; position in the batch has halted, because the GPU runs all positions in parallel and can't just skip one. With 128 positions in the prompt, the probability that every single position happens to halt simultaneously is effectively zero, so the early-exit path never fires. Second, the halting decision is made by a learned predictor, and my model was randomly initialized (not trained). An untrained model doesn't know which tokens are easy. You'd need actual training plus a mix of easy-and-hard positions for ACT to help. Neither was present in my experiments.&lt;/p&gt;
&lt;h3&gt;The Architectural Ceiling I Didn't Expect&lt;/h3&gt;
&lt;p&gt;While trying to run 24 loops for an ablation that's not in this post, I hit this error:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;IndexError&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dimension&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It turned out the architecture has a small per-loop adaptation component, a tiny learnable "tweak" that's different for each loop iteration, letting loop #1 behave slightly differently from loop #8. That component is implemented as a lookup table with exactly &lt;code&gt;max_loop_iters&lt;/code&gt; entries. If you configured the model to train on 16 loops, you have 16 entries in the table. Trying to run a 17th loop means looking up entry 16 in a 16-entry table, which fails.&lt;/p&gt;
&lt;p&gt;This matters because one of the headline claims about looped transformers is &lt;em&gt;depth extrapolation&lt;/em&gt;: train the model on (say) 5-step reasoning chains, then at inference time let it run 10 or 20 loops to handle harder problems than it ever saw during training. The theoretical argument is that running more loops = more reasoning depth, and this should emerge as a free capability at inference time.&lt;/p&gt;
&lt;p&gt;The OpenMythos implementation supports depth extrapolation only up to &lt;code&gt;max_loop_iters&lt;/code&gt;. Past that, the per-loop adaptation lookup fails. You can extend the table, but only by reinitializing it larger and resuming training. You cannot simply crank a knob at inference time.&lt;/p&gt;
&lt;p&gt;That's a genuine constraint on the "more loops = deeper reasoning at inference" story, and it's the kind of thing you find only by trying to cross the boundary. Nothing in the README warns you about it. It's the sort of detail that disappears when a paper's theoretical claim ("more loops at inference!") becomes a concrete implementation ("a table indexed by loop number, which has a fixed size").&lt;/p&gt;
&lt;h3&gt;What I Could Not Verify&lt;/h3&gt;
&lt;p&gt;Nothing I did tells you anything about Claude Mythos.&lt;/p&gt;
&lt;p&gt;The architecture OpenMythos implements could be exactly the Mythos architecture. It could be a reasonable guess that shares some features with Mythos. It could be entirely wrong. You and I have no way to check, because Anthropic has not published the architecture. The &lt;code&gt;mythos_1b&lt;/code&gt; I trained is a 1B-parameter looped transformer that &lt;em&gt;behaves&lt;/em&gt; consistently with published research on looped transformers. It is not Mythos.&lt;/p&gt;
&lt;p&gt;This is the epistemic limit that the repo's disclaimer is trying to name. Running a speculative reconstruction tells you whether the reconstruction is internally coherent, and whether it matches the published research it claims to match. It tells you nothing about whether the reconstruction maps to the thing it was reconstructed from. No amount of running it closes that gap. The gap is closed only by information the model's creators chose not to release, and running silicon against latent belief doesn't produce that information.&lt;/p&gt;
&lt;p&gt;So "I verified that OpenMythos's architecture works as claimed" is a real and useful statement. "I verified that Claude Mythos uses this architecture" is not something I can say, and nobody outside Anthropic can, no matter how thoroughly they run the reconstruction.&lt;/p&gt;
&lt;h3&gt;What an Open Reconstruction Is Good For&lt;/h3&gt;
&lt;p&gt;It's good for three things.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One, as a teaching artifact.&lt;/strong&gt; There's a live research line on looped transformers. Most descriptions of it are paper-shaped: dense with notation, theorem statements, ablation tables. OpenMythos is one of the few places you can read the whole architecture as working code in a single file, with every piece named and addressable. The stability guarantee that takes several pages of the Parcae paper to motivate resolves to one line of Python:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_A&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log_dt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log_A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's the whole guarantee. You can read it, you can run it, you can verify it on your bench. The paper claim goes from abstract math to a concrete object you can measure. For anyone who wants to understand why looped transformers work and has been bouncing off the academic literature, that's worth the install.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Two, as a testbed for your own ideas.&lt;/strong&gt; If you want to try modifying the architecture (swap which attention variant it uses, change how the experts are routed, make the loops behave differently at different depths) the code is about 1000 lines of clean PyTorch. You don't need to build a looped transformer from scratch; you can start from a working baseline and modify. The research is live and you can participate in it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Three, as a way to calibrate your expectations.&lt;/strong&gt; My 1B-parameter &lt;code&gt;mythos_1b&lt;/code&gt; produces 62 tokens per second at 16 loops on the 8060S. A full-scale Mythos would be far larger and presumably run at similar or more loops per token. If Mythos is actually a Recurrent-Depth Transformer, that tells you something about the real cost of running it: every token takes the full loop count of compute, regardless of how "easy" it is. That's a different cost shape than a standard transformer, which uses the same compute per token but over a fixed number of distinct layers. You can form a rough sense of the compute-per-token ratio that a looped architecture would imply for a frontier deployment, which is useful even if you never run a frontier model yourself.&lt;/p&gt;
&lt;p&gt;None of those three things are "I now know how Claude Mythos works." They are all "I now know things about looped transformers that I did not know before." For the blogger running an 8060S in a home lab, that's the realistic upside, and it's a larger upside than zero.&lt;/p&gt;
&lt;h3&gt;Coda: The Reconstruction as Thing&lt;/h3&gt;
&lt;p&gt;I wrote a &lt;a href="https://tinycomputers.io/posts/the-thing-and-the-endpoint.html"&gt;philosophy piece earlier this week&lt;/a&gt; about Heidegger's distinction between things and endpoints. A Z80 on a RetroShield is a thing: it gathers a world of silicon, engineers, software history, and your own hands. A cloud API is an endpoint: it offers a contract and deliberately hides everything behind it.&lt;/p&gt;
&lt;p&gt;Claude Mythos is an endpoint. You send tokens, you get tokens, the weights are not yours, the architecture is not yours, and if Anthropic swaps the backing model nothing changes for the caller by design. That's the whole value proposition. It refuses to gather.&lt;/p&gt;
&lt;p&gt;OpenMythos is a thing. I have its weights. I know the parameter count down to the last entry: 1,064,028,034. I measured its internal stability matrix at initialization and watched it move across training. I watched the hidden state blow up to 23,000 when I forced the instability and disabled the halting mechanism. I know how long a forward pass takes on my specific GPU with my specific ROCm wheel on a specific Tuesday in April 2026. It gathers a whole lineage: the 2026 Parcae paper that explained the stability trick, the older research on looped transformers that it built on, Kye Gomez's speculative synthesis of the two into a candidate architecture for Mythos, the AMD gfx1151 toolchain that lets me run any of this on a Ryzen APU at all, the one-line patch I had to apply to the code, my own debugging session, and thirty minutes of my GPU's fans running at full tilt.&lt;/p&gt;
&lt;p&gt;The thing gathers. What it gathers, though, is the &lt;em&gt;reconstruction&lt;/em&gt;. Not the reconstructed. My 1B model is a physical artifact with measurable behavior. It is not a window onto Anthropic's internals. Those internals remain an endpoint, and the endpoint remains abstract.&lt;/p&gt;
&lt;p&gt;Mythology intact. Architecture verified. That is what a home lab buys you in 2026.&lt;/p&gt;</description><category>ai</category><category>claude</category><category>looped transformer</category><category>mythos</category><category>openmythos</category><category>philosophy</category><category>reconstruction</category><category>recurrent depth transformer</category><category>rocm</category><category>strix halo</category><guid>https://tinycomputers.io/posts/architecture-verified-mythology-intact.html</guid><pubDate>Mon, 20 Apr 2026 13:00:00 GMT</pubDate></item><item><title>Processing 51,000 Photos with AI on AMD Strix Halo</title><link>https://tinycomputers.io/posts/processing-51000-photos-with-ai-on-amd-strix-halo.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/processing-51000-photos-with-ai-on-amd-strix-halo_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;17 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;I have roughly 20 years of photos sitting on a home fileserver. They span 2001 to 2020, shot on everything from a &lt;a href="https://baud.rs/StrgMz"&gt;Minolta&lt;/a&gt; DiMAGE F100 to a &lt;a href="https://baud.rs/qJQjcb"&gt;Nikon D5100&lt;/a&gt; to various iPhones over the years. A mix of 21,554 JPEGs and 29,860 Nikon RAW files (51,414 images total) organized in a &lt;a href="https://amzn.to/4lwULpW"&gt;Lightroom&lt;/a&gt; backup directory by year, month, and date. Most were shot handheld, many in a hurry. The kind of archive that accumulates when you take photos for two decades without ever going back to curate them.&lt;/p&gt;
&lt;p&gt;The Lightroom catalog that once made sense of all this was long gone, lost to a drive migration somewhere around 2018. What remained was a directory tree of raw files with no organization beyond the date folders. No star ratings, no keywords, no collections. Just files. Thousands of them, some sideways, some crooked, all unlabeled.&lt;/p&gt;
&lt;p&gt;I wanted to fix that. Not manually (I don't have a month to spend in Lightroom) but programmatically. The goals were straightforward: correct orientation issues, straighten crooked horizons, generate AI descriptions of every photo's content, and catalog the whole archive in a queryable database. The kind of batch processing job that would have been impractical five years ago but is now entirely doable with the right hardware and a weekend of scripting.&lt;/p&gt;
&lt;h3&gt;The Hardware&lt;/h3&gt;
&lt;p&gt;Two machines on the local network, each with a distinct role:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Machine&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Key Specs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fileserver&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NAS / photo storage&lt;/td&gt;
&lt;td&gt;28TB RAID (&lt;code&gt;/md0&lt;/code&gt;), 125GB RAM, NFS exports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPU workstation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ML inference&lt;/td&gt;
&lt;td&gt;&lt;a href="https://baud.rs/6jjmD9"&gt;AMD Ryzen AI Max+ 395&lt;/a&gt;, Radeon 8060S, 121GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The fileserver is a straightforward storage box. The interesting machine is the GPU workstation running an AMD Strix Halo APU, specifically the AI Max+ 395 with its integrated Radeon 8060S. I've written about this chip &lt;a href="https://tinycomputers.io/posts/amd-ai-max+-395-system-review-a-comprehensive-analysis.html"&gt;before&lt;/a&gt;, and it continues to impress for inference workloads. The RDNA 3.5 integrated GPU shares system memory, giving it access to 65.2 GB of VRAM without the typical constraints of a discrete card. For a model like BLIP that needs maybe 2 GB, that's absurdly generous, but it means you never have to think about VRAM budgets, which is a luxury when you're iterating on a processing pipeline.&lt;/p&gt;
&lt;p&gt;The fileserver already had NFS configured, exporting &lt;code&gt;/md0&lt;/code&gt; to the local subnet. One mount command on the GPU workstation and both machines could see the same filesystem:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;mount&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;nfs&lt;span class="w"&gt; &lt;/span&gt;fileserver.localnet:/md0&lt;span class="w"&gt; &lt;/span&gt;/md0
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;No file copying, no rsync scripts, no staging directories. The photos live on the NAS and get processed in-place over the network. Gigabit Ethernet introduces some I/O overhead (each 25 MB NEF file takes 200–300ms to read across the wire), but for an overnight batch job, the simplicity of a single shared filesystem is worth the throughput trade-off. If this were a recurring workflow, I'd invest in 10GbE, but for a one-time archive processing run, gigabit got it done.&lt;/p&gt;
&lt;h3&gt;The Software Stack&lt;/h3&gt;
&lt;p&gt;Everything runs in a Python virtual environment on the GPU workstation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PyTorch 2.9.1+rocm6.3&lt;/strong&gt;: ML framework with AMD ROCm backend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BLIP&lt;/strong&gt; (&lt;a href="https://huggingface.co/Salesforce/blip-image-captioning-large"&gt;&lt;code&gt;Salesforce/blip-image-captioning-large&lt;/code&gt;&lt;/a&gt;): vision-language model for image captioning&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenCV 4.13&lt;/strong&gt;: horizon detection via Canny edge detection and Hough transforms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rawpy 0.26.1&lt;/strong&gt;: Nikon NEF/NRW decoding (wraps LibRaw)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;piexif&lt;/strong&gt;: EXIF metadata extraction for JPEGs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;exiftool&lt;/strong&gt;: EXIF extraction for RAW files (called as a subprocess)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQLite&lt;/strong&gt;: metadata and results database&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;The gfx1151 Situation&lt;/h4&gt;
&lt;p&gt;If you've followed my &lt;a href="https://tinycomputers.io/posts/getting-pytorch-working-with-amd-radeon-pro-w7900-max+-395-a-comprehensive-guide.html"&gt;previous posts on Strix Halo&lt;/a&gt;, you know the drill. The Radeon 8060S reports as &lt;code&gt;gfx1151&lt;/code&gt; in ROCm, which is newer than what PyTorch's ROCm wheels officially target. The fix is the same environment variable override:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;HSA_OVERRIDE_GFX_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;11&lt;/span&gt;.0.0
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This maps the GPU to a generic gfx11 target. In practice, it works without issues, with no compute errors and no performance penalties. ROCm 6.16 on this machine also reports &lt;code&gt;amdgcn-amd-amdhsa--gfx11-generic&lt;/code&gt; as a supported ISA, which is likely why the override works cleanly. I've been running production workloads with this flag for months now without incident.&lt;/p&gt;
&lt;h3&gt;The Processing Pipeline&lt;/h3&gt;
&lt;p&gt;Each photo passes through five stages: EXIF extraction, orientation correction, horizon detection and straightening, AI captioning, and finally saving the corrected image and cataloging everything in SQLite.&lt;/p&gt;
&lt;h4&gt;EXIF Metadata Extraction&lt;/h4&gt;
&lt;p&gt;For JPEGs, &lt;code&gt;piexif&lt;/code&gt; reads the embedded EXIF data directly; it's a pure Python library that parses the binary EXIF structure without needing any external dependencies. For NEF/NRW files, piexif can't handle Nikon's proprietary container format, so I shell out to &lt;code&gt;exiftool&lt;/code&gt; with JSON output (&lt;code&gt;exiftool -json -n &amp;lt;file&amp;gt;&lt;/code&gt;). The &lt;code&gt;-n&lt;/code&gt; flag is important; it returns numeric values instead of human-readable strings, which makes downstream processing much cleaner.&lt;/p&gt;
&lt;p&gt;The extracted fields cover the full gamut: camera make and model, lens, dates, exposure settings (shutter speed, aperture, ISO, focal length), flash, white balance, metering mode, GPS coordinates, and the original orientation tag.&lt;/p&gt;
&lt;p&gt;EXIF data is notoriously inconsistent across two decades of cameras. I'll come back to this; it became a debugging story of its own.&lt;/p&gt;
&lt;h4&gt;Orientation Correction&lt;/h4&gt;
&lt;p&gt;The EXIF orientation tag (values 1 through 8) encodes how the camera was held when the photo was taken. A value of 1 means the image is right-side up. A value of 6 means the camera was rotated 90 degrees clockwise. Value 3 means 180 degrees. Some values encode horizontal or vertical flips. The full matrix looks like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;ops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FLIP_LEFT_RIGHT&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ROTATE_180&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FLIP_TOP_BOTTOM&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FLIP_LEFT_RIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ROTATE_270&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ROTATE_270&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FLIP_LEFT_RIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ROTATE_90&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ROTATE_90&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Out of the 51,411 successfully processed photos, &lt;strong&gt;8,797 (17.1%) needed orientation correction&lt;/strong&gt;. The majority came from the Nikon D5100 and iPhone 4, both of which set the orientation tag but don't bake the rotation into the pixel data itself. Without this correction, nearly one in five photos would display sideways or upside-down in any viewer that doesn't respect EXIF orientation.&lt;/p&gt;
&lt;p&gt;Here's what that looks like in practice. The raw pixel data from this iPhone photo is stored sideways; the camera recorded an EXIF orientation tag of 6, meaning "rotate 90 degrees clockwise to display correctly." Any viewer that ignores that tag renders the image on its side:&lt;/p&gt;
&lt;div style="display: flex; gap: 10px; margin: 20px 0;"&gt;
&lt;div style="flex: 1; text-align: center;"&gt;
&lt;img src="https://tinycomputers.io/images/photo-proc-dog-before.jpg" alt="Dog photo with incorrect orientation - displayed sideways" style="max-width: 100%; box-shadow: 2px 2px 6px rgba(0,0,0,0.3);"&gt;
&lt;p&gt;&lt;em&gt;Before: raw pixel data (EXIF orientation 6, displayed sideways)&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div style="flex: 1; text-align: center;"&gt;
&lt;img src="https://tinycomputers.io/images/photo-proc-dog-after.jpg" alt="Dog photo after EXIF orientation correction - displayed upright" style="max-width: 100%; box-shadow: 2px 2px 6px rgba(0,0,0,0.3);"&gt;
&lt;p&gt;&lt;em&gt;After: orientation corrected based on EXIF tag&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;h4&gt;Horizon Detection and Straightening&lt;/h4&gt;
&lt;p&gt;This stage uses classical computer vision, no neural network needed. The approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Downscale the image to 1200px on the long side for speed&lt;/li&gt;
&lt;li&gt;Convert to grayscale, apply Gaussian blur&lt;/li&gt;
&lt;li&gt;Run Canny edge detection&lt;/li&gt;
&lt;li&gt;Crop to the vertical middle 50%, since the horizon is rarely at the extreme top or bottom of a frame&lt;/li&gt;
&lt;li&gt;Apply the Hough Line Transform to find line segments, requiring a minimum length of one-quarter the image width&lt;/li&gt;
&lt;li&gt;Filter to near-horizontal lines (within 20 degrees of level)&lt;/li&gt;
&lt;li&gt;Compute a weighted average of the detected angles, weighted by line length&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The key is the threshold window. If the detected angle is less than 0.5 degrees, it's not worth correcting, since you'd introduce interpolation artifacts for no visible benefit. If it's greater than 15 degrees, it's probably not a tilted horizon at all; it's either intentional composition or the algorithm latching onto a staircase railing. The correction itself uses &lt;code&gt;cv2.warpAffine&lt;/code&gt; with Lanczos interpolation and a reflective border mode, followed by an inward crop to eliminate any border artifacts:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;crop_factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The initial implementation used Canny edge detection and Hough line transforms, classical CV techniques from the 1980s. Fast, deterministic, 100ms per image. But it had a fatal flaw: it couldn't distinguish between a tilted horizon and a roofline receding toward a vanishing point. Architecture, roads, staircases, any strong line in the middle band of the image would register as a "tilted horizon," and the algorithm would dutifully rotate the image to "correct" it. In practice, this meant a significant number of photos were being made &lt;em&gt;worse&lt;/em&gt;, not better.&lt;/p&gt;
&lt;p&gt;The fix was to replace Hough line detection with semantic segmentation. SegFormer (&lt;code&gt;nvidia/segformer-b2-finetuned-ade-512-512&lt;/code&gt;), trained on the ADE20K dataset, segments each image into 150 classes, including sky. The approach is simple: find the sky pixels, trace the bottom edge of the sky region, fit a line to that boundary, and measure its angle. If there's no sky (less than 5% of the image), or the sky boundary is too fragmented (fewer than 20 points), skip the correction entirely.&lt;/p&gt;
&lt;p&gt;This eliminates false positives on indoor shots, close-ups, architecture, and anything without a visible sky. SegFormer runs on CPU at about 0.4 seconds per image; the model is only 25M parameters, so it doesn't need the GPU. The GPU stays dedicated to BLIP captioning.&lt;/p&gt;
&lt;p&gt;Two examples from the corrected archive. This bridge over a river had a 2.68-degree clockwise tilt, and the bridge deck and far shore are visibly leveled:&lt;/p&gt;
&lt;div style="display: flex; gap: 10px; margin: 20px 0;"&gt;
&lt;div style="flex: 1; text-align: center;"&gt;
&lt;img src="https://tinycomputers.io/images/photo-proc-river-before.jpg" alt="Bridge over river with tilted horizon" style="max-width: 100%; box-shadow: 2px 2px 6px rgba(0,0,0,0.3);"&gt;
&lt;p&gt;&lt;em&gt;Before: 2.68° clockwise tilt&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div style="flex: 1; text-align: center;"&gt;
&lt;img src="https://tinycomputers.io/images/photo-proc-river-after.jpg" alt="Bridge over river with corrected horizon" style="max-width: 100%; box-shadow: 2px 2px 6px rgba(0,0,0,0.3);"&gt;
&lt;p&gt;&lt;em&gt;After: horizon straightened via sky boundary detection&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;This rocky Lake Superior shore had a 3.85-degree clockwise tilt, and the far horizon is leveled:&lt;/p&gt;
&lt;div style="display: flex; gap: 10px; margin: 20px 0;"&gt;
&lt;div style="flex: 1; text-align: center;"&gt;
&lt;img src="https://tinycomputers.io/images/photo-proc-shore-before.jpg" alt="Rocky lakeshore with tilted horizon" style="max-width: 100%; box-shadow: 2px 2px 6px rgba(0,0,0,0.3);"&gt;
&lt;p&gt;&lt;em&gt;Before: 3.85° clockwise tilt&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div style="flex: 1; text-align: center;"&gt;
&lt;img src="https://tinycomputers.io/images/photo-proc-shore-after.jpg" alt="Rocky lakeshore with corrected horizon" style="max-width: 100%; box-shadow: 2px 2px 6px rgba(0,0,0,0.3);"&gt;
&lt;p&gt;&lt;em&gt;After: horizon straightened&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;h4&gt;AI Captioning with BLIP&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;Salesforce/blip-image-captioning-large&lt;/code&gt; model generates natural language descriptions of each photo. It runs in float16 on the Radeon 8060S. Each image is resized to a maximum of 1024px before inference. Beam search with 5 beams and a 75-token limit generates the caption:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;output_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_new_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;num_beams&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;early_stopping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Caption inference takes about 0.5–0.7 seconds per image, consistent regardless of whether the input was a JPEG or a decoded NEF. The model handles a wide variety of subjects surprisingly well. Some examples from the archive:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"a brown and white dog standing next to a blue chair"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"two silos sitting in the middle of a field"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"a bird sitting on a branch of a tree"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"a wooden sign that says hoban road in front of some trees"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"a blurry photo of a car driving down a snowy road"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"a dog being groomed by a woman in a salon"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The captions tend toward a "there is a..." pattern, and they occasionally get details wrong (BLIP once described a photo of my living room as "a hotel lobby," which is generous). But for searchability and cataloging purposes, they're remarkably useful. Being able to query &lt;code&gt;WHERE caption LIKE '%dog%'&lt;/code&gt; across 51,000 photos and get meaningful results is something that would have required manual tagging before models like BLIP existed. For an archive this size, "good enough" captions on every photo are vastly more useful than perfect captions on none of them.&lt;/p&gt;
&lt;h4&gt;Save and Catalog&lt;/h4&gt;
&lt;p&gt;Corrected images are saved as high-quality JPEGs (quality 92) to &lt;code&gt;/md0/photos_processed/images/&lt;/code&gt;, mirroring the original directory structure. NEF and NRW files are converted to JPEG in the process; the corrected archive is a uniform format. All metadata flows into a SQLite database with WAL journaling, tracking 40+ fields per photo: every piece of EXIF data, processing flags (was orientation corrected? was the horizon straightened? by how many degrees?), the AI caption, file hashes, dimensions, and processing timestamps.&lt;/p&gt;
&lt;p&gt;The database makes the archive queryable in ways that were never possible before:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;-- What cameras did I use, and when?&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;camera_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_taken&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_taken&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;photos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;GROUP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;camera_model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Photos with GPS data&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;caption&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gps_latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gps_longitude&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;photos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gps_latitude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;IS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- How crooked were my photos, by camera?&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;camera_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ABS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;horizon_angle_degrees&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;avg_tilt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;photos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;horizon_corrected&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;camera_model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;avg_tilt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;The EXIF Tuple Bug&lt;/h3&gt;
&lt;p&gt;The first processing pass completed 51,414 photos, but with 2,146 errors. All of them were &lt;code&gt;TypeError: type tuple doesn't define __round__ method&lt;/code&gt;. For a pipeline that had been running cleanly on thousands of Nikon D5100 and D60 photos, this was unexpected.&lt;/p&gt;
&lt;p&gt;The root cause turned out to be a two-part problem with how certain budget cameras from the 2008–2012 era write EXIF rational numbers.&lt;/p&gt;
&lt;h4&gt;Part 1: Malformed Tuples&lt;/h4&gt;
&lt;p&gt;The EXIF standard stores rational numbers as &lt;code&gt;(numerator, denominator)&lt;/code&gt; pairs. Most cameras follow this. But some, particularly a batch of older point-and-shoots, wrote the &lt;code&gt;ExposureBiasValue&lt;/code&gt; field as a 4-element tuple like &lt;code&gt;(36, 0, 18, 0)&lt;/code&gt; instead of the expected 2-element &lt;code&gt;(36, 0)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My &lt;code&gt;_rational_to_float&lt;/code&gt; helper only handled 2-tuples:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_rational_to_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;  &lt;span class="c1"&gt;# passes through 4-tuples as raw tuples&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When a 4-tuple fell through, the downstream &lt;code&gt;round()&lt;/code&gt; call choked on it. The fix was simple: return &lt;code&gt;None&lt;/code&gt; for any tuple that isn't a standard rational pair.&lt;/p&gt;
&lt;h4&gt;Part 2: None Propagation&lt;/h4&gt;
&lt;p&gt;Even after fixing Part 1, many of these same cameras had written &lt;code&gt;(36, 0)&lt;/code&gt;, a rational with a zero denominator. The function correctly returned &lt;code&gt;None&lt;/code&gt; for division by zero, but the calling code then did &lt;code&gt;round(None, 2)&lt;/code&gt;, triggering the same &lt;code&gt;TypeError&lt;/code&gt; with a slightly different message.&lt;/p&gt;
&lt;p&gt;The fix was a &lt;code&gt;_safe_round&lt;/code&gt; wrapper:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_safe_round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After both fixes, the second pass recovered all 2,143 photos. The remaining 3 errors were genuine file corruption: a truncated JPEG, a NEF that LibRaw couldn't parse, and a NEF with filesystem-level I/O errors. Probably bad sectors on the source drive. Those can't be fixed in code.&lt;/p&gt;
&lt;p&gt;This is one of those bugs that only surfaces at scale. Run the pipeline on a hundred Nikon photos and everything works perfectly. Run it on 51,000 photos spanning 15 different camera models over 20 years, and every edge case in the EXIF spec comes out to play. The lesson, which I should have internalized long ago: never trust external data formats at scale without defensive parsing on every field. The EXIF spec is a suggestion, not a contract, and camera manufacturers have been interpreting it creatively since the early 2000s.&lt;/p&gt;
&lt;h3&gt;Resumability&lt;/h3&gt;
&lt;p&gt;A 15-hour batch job will inevitably need to be restarted: bugs, system updates, a random hound disconnects the magsafe power cord from my MacBook Pro. The script tracks progress in SQLite and skips completed files on restart:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;is_already_processed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"SELECT id FROM photos WHERE source_path = ? AND error IS NULL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_path&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Photos that failed with errors are intentionally &lt;em&gt;not&lt;/em&gt; skipped, so fixing a bug and re-running automatically retries them. This made the EXIF debugging cycle painless: fix the parser, clear the failed rows from the database, relaunch, and only the 2,143 affected photos get reprocessed.&lt;/p&gt;
&lt;h3&gt;Performance&lt;/h3&gt;
&lt;p&gt;The pipeline sustained &lt;strong&gt;1.0–1.8 photos per second&lt;/strong&gt;, depending on file format:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Time per Photo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JPEG load&lt;/td&gt;
&lt;td&gt;~10ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NEF decode (rawpy)&lt;/td&gt;
&lt;td&gt;~400ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MD5 hash&lt;/td&gt;
&lt;td&gt;~5ms (JPEG), ~100ms (NEF)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Horizon detection&lt;/td&gt;
&lt;td&gt;~100ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BLIP inference&lt;/td&gt;
&lt;td&gt;~500–700ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG save&lt;/td&gt;
&lt;td&gt;~50ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;BLIP inference dominates the runtime. NEF decoding is the second bottleneck; each RAW file is 20–30 MB and requires full demosaicing through LibRaw. The NFS overhead for reading large NEFs over gigabit Ethernet is noticeable but not the primary constraint.&lt;/p&gt;
&lt;p&gt;Total wall time: &lt;strong&gt;15.5 hours&lt;/strong&gt; across two passes for 51,414 photos. The BLIP model uses roughly 2 GB of the 65.2 GB available VRAM on the Strix Halo. Memory was never a concern.&lt;/p&gt;
&lt;h3&gt;Final Results&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Percentage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total photos&lt;/td&gt;
&lt;td&gt;51,414&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Successfully processed&lt;/td&gt;
&lt;td&gt;51,411&lt;/td&gt;
&lt;td&gt;99.99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orientation corrected&lt;/td&gt;
&lt;td&gt;8,797&lt;/td&gt;
&lt;td&gt;17.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Horizon straightened&lt;/td&gt;
&lt;td&gt;15,251&lt;/td&gt;
&lt;td&gt;29.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI captioned&lt;/td&gt;
&lt;td&gt;51,411&lt;/td&gt;
&lt;td&gt;99.99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unrecoverable errors&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0.006%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The top cameras in the archive tell the story of 20 years of gear:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Camera&lt;/th&gt;
&lt;th&gt;Photos&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://baud.rs/qJQjcb"&gt;Nikon D5100&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;24,073&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://baud.rs/mwoMko"&gt;Nikon D60&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;8,734&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPhone 4&lt;/td&gt;
&lt;td&gt;2,664&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://baud.rs/ACrtrD"&gt;Nikon D3100&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1,698&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://baud.rs/jxhHU5"&gt;Panasonic DMC-FX07&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;975&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://baud.rs/StrgMz"&gt;Minolta DiMAGE F100&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;870&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPad&lt;/td&gt;
&lt;td&gt;803&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPhone 5s&lt;/td&gt;
&lt;td&gt;698&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://baud.rs/10it3U"&gt;Samsung SCH-I500&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;645&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The output lives on the NAS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Corrected images&lt;/strong&gt;: &lt;code&gt;/md0/photos_processed/images/&lt;/code&gt;, 51,411 JPEGs preserving the original year/month/date folder structure, all NEFs converted, all orientation and horizon corrections applied.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQLite database&lt;/strong&gt;: &lt;code&gt;/md0/photos_processed/photos.db&lt;/code&gt;, 40+ fields per photo with full EXIF metadata, processing results, and AI-generated captions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Processing log&lt;/strong&gt;: &lt;code&gt;/md0/photos_processed/processing.log&lt;/code&gt;, timestamped record of the entire run.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Takeaways&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;AMD's Strix Halo continues to earn its keep for ML inference.&lt;/strong&gt; The &lt;code&gt;HSA_OVERRIDE_GFX_VERSION=11.0.0&lt;/code&gt; workaround remains necessary, but once set, PyTorch and ROCm run without complaints. The 65 GB shared VRAM pool means you can load models without thinking about memory budgets, a workflow advantage that's easy to underestimate until you've experienced it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Classical computer vision still has its place.&lt;/strong&gt; The horizon detection pipeline uses Canny edge detection and Hough transforms, techniques from the 1980s. No training data, no GPU needed, deterministic results, and the whole thing runs in 100ms per image. For geometric corrections on photographic images, you don't need a neural network. You need line detection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EXIF is a minefield.&lt;/strong&gt; Twenty years of cameras from different manufacturers means every edge case in the spec gets exercised. Tuple lengths vary, denominators are zero, fields are missing or repurposed. If you're parsing EXIF at scale, assume nothing about the data's shape and validate everything.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Resumability is non-negotiable for long-running jobs.&lt;/strong&gt; Tracking progress in the database and skipping completed work made it trivial to iterate on bugs. Without this, every fix would mean reprocessing 51,000 photos from scratch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NFS over gigabit is fine for batch processing.&lt;/strong&gt; Not optimal, but for an overnight job, the network overhead from NAS-attached storage is acceptable. The real bottleneck was ML inference at 0.6 seconds per photo. If I were doing this regularly, 10GbE would be worth the upgrade, but for a one-time archive processing run, gigabit got the job done.&lt;/p&gt;
&lt;p&gt;The whole project, from first SSH to final database entry, took about a day of wall time, most of which was unattended processing. The scripting itself was maybe three hours of work. Twenty years of photos, cataloged and corrected overnight. Not bad for a Strix Halo and some Python. The full source is available on &lt;a href="https://github.com/ajokela/photo-processor"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;What I didn't expect was how useful the database would be after the fact. Being able to ask "show me every photo I took with the D5100 at ISO 3200 or higher" or "find photos with GPS data from 2015" turns a pile of files into something that actually tells a story. The AI captions add another dimension; I can now search my own photo archive by content, not just metadata. It's the kind of capability that makes you wonder why photo management software hasn't done this for years. The models have been available. The hardware has been affordable. Someone just needed to wire it together.&lt;/p&gt;</description><category>ai max+ 395</category><category>amd</category><category>blip</category><category>computer vision</category><category>exif</category><category>image captioning</category><category>machine learning</category><category>nef</category><category>nikon</category><category>opencv</category><category>photography</category><category>pytorch</category><category>rocm</category><category>sqlite</category><category>strix halo</category><guid>https://tinycomputers.io/posts/processing-51000-photos-with-ai-on-amd-strix-halo.html</guid><pubDate>Sat, 14 Mar 2026 17:00:00 GMT</pubDate></item><item><title>The Real Cost of Running Qwen TTS Locally: Three Machines Compared</title><link>https://tinycomputers.io/posts/the-real-cost-of-running-qwen-tts-locally-three-machines-compared.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/the-real-cost-of-running-qwen-tts-locally-three-machines-compared_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;17 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;&lt;img src="https://tinycomputers.io/images/qwen-tts-benchmark/p40-server-shop.jpg" alt="The Tesla P40 server standing on its side in an unheated Minnesota shop building, one of three machines benchmarked for local TTS generation" style="float: right; max-width: 40%; margin: 0 0 1em 1.5em; border-radius: 4px; box-shadow: 0 30px 40px rgba(0,0,0,.1);"&gt;&lt;/p&gt;
&lt;p&gt;Every post on this site has an audio version. A small player at the top, a few minutes of narration, generated entirely on local hardware. No cloud API, no per-character fees, no data leaving the network. I wrote about &lt;a href="https://tinycomputers.io/posts/qwen-tts-on-amd-strix-halo.html"&gt;setting up the pipeline on AMD Strix Halo&lt;/a&gt; earlier this year, and the system has been running in production since, generating narrations for new posts, regenerating old ones when I revise them, and occasionally processing long-form content that would cost real money through Google Cloud TTS or ElevenLabs.&lt;/p&gt;
&lt;p&gt;But I now have three machines capable of running Qwen3-TTS, and they could not be more different from each other. An Apple M3 Max laptop. An AMD Ryzen AI MAX+ 395 mini desktop with integrated Radeon graphics. And a &lt;a href="https://tinycomputers.io/posts/repurposing-enterprise-gpus-the-tesla-p40-home-lab-story.html"&gt;four-GPU Tesla P40 server&lt;/a&gt; built from decade-old enterprise hardware bought on eBay. Three different silicon vendors, three different compute backends (MPS, ROCm, and CUDA) running the same model on the same text.&lt;/p&gt;
&lt;p&gt;The question I wanted to answer is simple: how do they actually compare? Not on paper. Not in theoretical FLOPS. In wall-clock time, generating real audio from a real blog post.&lt;/p&gt;
&lt;p&gt;The answer turned out to be more interesting than I expected, because the numbers tell a story about hardware architecture that raw specifications completely miss.&lt;/p&gt;
&lt;h3&gt;The Setup&lt;/h3&gt;
&lt;p&gt;The model is &lt;a href="https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"&gt;Qwen3-TTS-12Hz-1.7B-CustomVoice&lt;/a&gt;, a 1.7 billion parameter autoregressive text-to-speech model from Alibaba's Qwen team. It generates natural-sounding speech with multiple speaker voices. I use the Eric voice for all blog narrations: clear, professional, well-paced for technical content.&lt;/p&gt;
&lt;p&gt;The three machines:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Apple M3 Max&lt;/strong&gt;, a &lt;a href="https://amzn.to/4rwlTa6"&gt;MacBook Pro&lt;/a&gt; with Apple's M3 Max chip. 14 CPU cores, 30 GPU cores, 64GB unified memory. The GPU runs through PyTorch's MPS (Metal Performance Shaders) backend. This is my daily driver laptop, and it generates TTS when I am writing and editing posts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AMD Radeon 8060S&lt;/strong&gt;, a Bosgame M5 mini desktop running &lt;a href="https://amzn.to/4bv5CMG"&gt;AMD's Ryzen AI MAX+ 395&lt;/a&gt;. This is a Strix Halo APU with integrated RDNA 3.5 graphics, not a discrete GPU. It shares 128GB of DDR5 system memory with the CPU, with roughly 96GB addressable as VRAM. The GPU runs through ROCm 7.2 with PyTorch 2.9.1. The gfx1151 architecture requires specific PyTorch wheels from AMD's pre-release index and several environment variable overrides to function. I wrote a &lt;a href="https://tinycomputers.io/posts/qwen-tts-on-amd-strix-halo.html"&gt;full setup guide&lt;/a&gt; for this machine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NVIDIA Tesla P40&lt;/strong&gt;, a 2U rack-mount server with four &lt;a href="https://www.ebay.com/itm/306087510352?_skw=nvidia+tesla+p40+24gb+gpu&amp;amp;epid=27032254618&amp;amp;itmmeta=01KKJEGQKSK110HNM6214EB0TT&amp;amp;hash=item47443cc150:g:qAwAAOSwy0toUHXh&amp;amp;itmprp=enc%3AAQALAAABAGfYFPkwiKCW4ZNSs2u11xAq6UjArKrgnuEyMVTZhAZhOSUGYags6TsDJvvCEOa51UH2r%2BRe%2F182ah6rgiTIAIRULQNEL9rbiinCXMor%2FBNNZk0GaNKqTWkq9pLWGoRBM8NL%2BjC1aSA63XPe4YsFHjQkb%2Fmup21S3UM7oqwBrW%2BHep1E07lnrt2vzkljSA4xg7SnrA%2BFDtOdqvDwO4tpgB0t%2BtCv9%2BlXoh%2BeoEgpJqXgaaM0ad48OfmgKB13PF9RIPXLNI6z4SjV2O%2FXOk6nYPyD9Eg5wbzdmsXfNRhwitz7HEZ1bTRUnRmvKzQrw4B3r3LAag5f8%2B8CcCWfCRAkkG8%3D%7Ctkp%3ABk9SR4j6ws6cZw&amp;amp;mkcid=1&amp;amp;mkrid=711-53200-19255-0&amp;amp;siteid=0&amp;amp;campid=5338960379&amp;amp;customid=&amp;amp;toolid=10001&amp;amp;mkevt=1"&gt;Tesla P40 GPUs&lt;/a&gt;, each with 24GB of GDDR5X. Pascal architecture from 2016. Compute capability 6.1. No Tensor Cores, no native bfloat16 support. The benchmark uses a single P40, since Qwen TTS runs on one GPU. This machine lives in an unheated shop building in Minnesota and &lt;a href="https://tinycomputers.io/posts/repurposing-enterprise-gpus-the-tesla-p40-home-lab-story.html"&gt;screams through the winter&lt;/a&gt; when the BMC misinterprets sub-zero ambient temperatures as a hardware malfunction.&lt;/p&gt;
&lt;p&gt;All three machines run the same model checkpoint, the same text input, and the same speaker voice. The only differences are the silicon and the compute backend.&lt;/p&gt;
&lt;h3&gt;The Benchmark&lt;/h3&gt;
&lt;p&gt;I used a standardized 2,411-character passage, five paragraphs on the Jevons Paradox, dense enough to exercise the model's prosody and pacing on real written content. Each machine ran three consecutive generations from the same loaded model, producing roughly three minutes of audio per run. The first run includes kernel compilation and cache warmup; subsequent runs reflect steady-state performance.&lt;/p&gt;
&lt;p&gt;The metric that matters is Real-Time Factor (RTF): how many seconds of wall-clock time it takes to generate one second of audio. An RTF of 1.0 means the model generates audio at exactly real-time speed. Below 1.0 is faster than real-time. Above 1.0 means you are waiting.&lt;/p&gt;
&lt;h4&gt;Individual Runs&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Apple M3 Max (MPS)&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Generation Time&lt;/th&gt;
&lt;th&gt;Audio Length&lt;/th&gt;
&lt;th&gt;RTF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;698.5s&lt;/td&gt;
&lt;td&gt;197.7s&lt;/td&gt;
&lt;td&gt;3.53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;533.1s&lt;/td&gt;
&lt;td&gt;184.2s&lt;/td&gt;
&lt;td&gt;2.89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;447.8s&lt;/td&gt;
&lt;td&gt;179.2s&lt;/td&gt;
&lt;td&gt;2.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Average&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;559.8s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;187.0s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.97&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;AMD Radeon 8060S (ROCm)&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Generation Time&lt;/th&gt;
&lt;th&gt;Audio Length&lt;/th&gt;
&lt;th&gt;RTF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;729.2s&lt;/td&gt;
&lt;td&gt;173.6s&lt;/td&gt;
&lt;td&gt;4.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;460.0s&lt;/td&gt;
&lt;td&gt;204.8s&lt;/td&gt;
&lt;td&gt;2.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;548.2s&lt;/td&gt;
&lt;td&gt;214.2s&lt;/td&gt;
&lt;td&gt;2.56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Average&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;579.1s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;197.5s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;NVIDIA Tesla P40 (CUDA)&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Generation Time&lt;/th&gt;
&lt;th&gt;Audio Length&lt;/th&gt;
&lt;th&gt;RTF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1511.4s&lt;/td&gt;
&lt;td&gt;204.1s&lt;/td&gt;
&lt;td&gt;7.41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1225.7s&lt;/td&gt;
&lt;td&gt;171.6s&lt;/td&gt;
&lt;td&gt;7.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1537.2s&lt;/td&gt;
&lt;td&gt;206.7s&lt;/td&gt;
&lt;td&gt;7.44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Average&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1424.8s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;194.1s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.33&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Summary&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Machine&lt;/th&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;Avg RTF&lt;/th&gt;
&lt;th&gt;Best RTF&lt;/th&gt;
&lt;th&gt;Avg Gen Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MacBook Pro&lt;/td&gt;
&lt;td&gt;M3 Max (MPS)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.97&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.50&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;559.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bosgame M5&lt;/td&gt;
&lt;td&gt;Radeon 8060S (ROCm)&lt;/td&gt;
&lt;td&gt;3.00&lt;/td&gt;
&lt;td&gt;2.25&lt;/td&gt;
&lt;td&gt;579.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Penguin 2U&lt;/td&gt;
&lt;td&gt;Tesla P40 (CUDA)&lt;/td&gt;
&lt;td&gt;7.33&lt;/td&gt;
&lt;td&gt;7.14&lt;/td&gt;
&lt;td&gt;1424.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;What the Numbers Mean&lt;/h3&gt;
&lt;p&gt;The headline result is that the M3 Max and Radeon 8060S are essentially tied, and the Tesla P40 is roughly 2.4 times slower than both. But that summary hides the interesting details.&lt;/p&gt;
&lt;h4&gt;The Warmup Effect Is Massive&lt;/h4&gt;
&lt;p&gt;On both the M3 Max and the Radeon 8060S, the first run is dramatically slower than subsequent runs. The M3 Max goes from RTF 3.53 on run 1 to RTF 2.50 on run 3, a 29% improvement. The AMD shows an even larger swing: RTF 4.20 on run 1 dropping to RTF 2.25 on run 2, a 46% improvement.&lt;/p&gt;
&lt;p&gt;This is kernel compilation. Both MPS and ROCm compile GPU kernels on first use and cache them for subsequent calls. The Qwen TTS model hits a wide variety of kernel shapes during autoregressive generation (different sequence lengths, different attention patterns) and each new shape triggers a compilation on the first encounter. By run 2, most of the common shapes are cached, and performance stabilizes.&lt;/p&gt;
&lt;p&gt;The P40 shows almost no warmup effect. RTF 7.41 on run 1, 7.14 on run 2, 7.44 on run 3. CUDA's kernel compilation is faster and more mature, so the overhead is absorbed within the first few seconds rather than spread across the entire run. But this maturity does not translate into faster inference; CUDA compiles faster, but the P40's hardware is fundamentally slower at the operations this model requires.&lt;/p&gt;
&lt;p&gt;This has a practical implication that matters: &lt;strong&gt;short benchmarks on MPS and ROCm are misleading.&lt;/strong&gt; I initially ran a quick 276-character test on all three machines before doing the full benchmark. The short test showed the AMD at RTF 9.20, almost identical to the P40's RTF 10.01, and far behind the M3 Max's RTF 2.84. That result nearly led me to conclude the AMD was performing as poorly as decade-old hardware. The longer benchmark, with its warmup effect amortized across more generation, revealed the truth: the AMD is just as fast as the M3 Max once the kernels are cached. If I had stopped at the short test, I would have drawn exactly the wrong conclusion.&lt;/p&gt;
&lt;h4&gt;Why the P40 Is So Slow&lt;/h4&gt;
&lt;p&gt;The Tesla P40 is a Pascal-generation GPU from 2016. It has 3,840 CUDA cores and 24GB of GDDR5X memory. On paper, it should be competitive; 12 TFLOPS of FP32 compute is not trivial. And for LLM inference through Ollama, the P40 &lt;a href="https://tinycomputers.io/posts/repurposing-enterprise-gpus-the-tesla-p40-home-lab-story.html"&gt;performs remarkably well&lt;/a&gt;, outperforming quad T4 instances on models up to 8B parameters.&lt;/p&gt;
&lt;p&gt;TTS is a different workload. Qwen3-TTS is an autoregressive transformer that generates audio tokens one at a time, each conditioned on all previous tokens. This means the inference is heavily memory-bandwidth bound during the decoding phase, and compute-bound during the attention and feedforward passes. The model is distributed in bfloat16 precision, which the P40 cannot compute natively; Pascal predates bfloat16 support entirely. PyTorch silently promotes bf16 operations to fp32 on the P40, roughly doubling the computation per operation and halving the effective throughput.&lt;/p&gt;
&lt;p&gt;The P40 also lacks the SDPA (Scaled Dot-Product Attention) hardware acceleration that newer architectures provide. On the M3 Max, MPS routes attention through Metal's optimized primitives. On the AMD, ROCm's AOTriton provides experimental flash attention support. On the P40, attention runs through standard CUDA kernels without any of these accelerations. For a model that generates thousands of autoregressive steps per audio clip, each involving a full attention pass over the growing sequence, this compounds dramatically.&lt;/p&gt;
&lt;p&gt;The P40 is not bad hardware. It is excellent hardware for the workloads it was designed for: batch inference on quantized LLMs where its 24GB of VRAM per card creates a memory advantage. But autoregressive TTS in bfloat16 hits every one of its architectural weaknesses simultaneously.&lt;/p&gt;
&lt;h4&gt;Unified Memory Wins This Workload&lt;/h4&gt;
&lt;p&gt;Both the M3 Max and the Radeon 8060S use unified memory architectures, where the CPU and GPU share the same physical memory pool. The M3 Max has 64GB of unified LPDDR5. The Radeon 8060S shares 128GB of DDR5 with the CPU, with roughly 96GB addressable as VRAM.&lt;/p&gt;
&lt;p&gt;For a 1.7B parameter model in bf16, the weights occupy roughly 3.4GB. The model fits comfortably on all three machines. But the autoregressive generation pattern creates a stream of intermediate activations (KV cache entries, attention scores, feedforward intermediates) that grow with the sequence length. On a unified memory architecture, these intermediates exist in the same memory space as the model weights, avoiding any PCIe transfer overhead. On the P40, every interaction between CPU and GPU crosses a PCIe 3.0 bus.&lt;/p&gt;
&lt;p&gt;For LLM inference, where the bottleneck is token generation throughput and the KV cache fits in VRAM, the P40's discrete memory is fine. For TTS, where the model generates hundreds of audio tokens per second of speech and the attention window grows continuously, the memory access pattern favors unified architectures.&lt;/p&gt;
&lt;p&gt;This is not a universal statement about unified versus discrete memory. A modern discrete GPU with HBM2e or GDDR6X and PCIe 4.0 or 5.0 would likely outperform both the M3 Max and the Radeon 8060S on this workload. The P40's problem is not that its memory is discrete; it is that its memory is slow and its bus is narrow by 2026 standards.&lt;/p&gt;
&lt;h3&gt;The Model Architecture Question&lt;/h3&gt;
&lt;p&gt;While benchmarking Qwen TTS, I also ran a quick comparison with &lt;a href="https://huggingface.co/SWivid/F5-TTS"&gt;F5-TTS&lt;/a&gt; on the AMD machine to sanity-check the results. F5-TTS is a flow-matching model, fundamentally different from Qwen's autoregressive approach. Where Qwen generates audio tokens sequentially, each conditioned on all previous tokens, F5 generates audio in parallel through an iterative refinement process.&lt;/p&gt;
&lt;p&gt;The difference is stark. On the same Radeon 8060S, the same text, the same hardware:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Generation Time&lt;/th&gt;
&lt;th&gt;Audio Length&lt;/th&gt;
&lt;th&gt;RTF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-TTS&lt;/td&gt;
&lt;td&gt;579.1s (avg)&lt;/td&gt;
&lt;td&gt;197.5s&lt;/td&gt;
&lt;td&gt;3.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F5-TTS&lt;/td&gt;
&lt;td&gt;17.4s&lt;/td&gt;
&lt;td&gt;27.2s&lt;/td&gt;
&lt;td&gt;0.64&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;F5-TTS is faster than real-time. Qwen3-TTS takes three times longer than the audio it produces. On normalized terms, F5 is roughly five times faster than Qwen at steady-state, and the gap widens on shorter content where Qwen's warmup overhead is proportionally larger.&lt;/p&gt;
&lt;p&gt;This is not an apples-to-apples quality comparison. Qwen3-TTS generally produces more natural prosody, better handling of complex sentence structures, and more consistent speaker identity across long passages. F5-TTS is excellent but can occasionally drift in voice character or pacing on very long content. For blog narration, both are well above the threshold of "good enough," and the quality difference is smaller than you might expect given the architectural gap.&lt;/p&gt;
&lt;p&gt;The point is that hardware is only half the story. The choice of model architecture can matter more than the choice of GPU. A flow-matching model on integrated AMD graphics outperforms an autoregressive model on Apple's best laptop silicon by a wide margin. If generation speed is the constraint, switching models gains more than switching hardware.&lt;/p&gt;
&lt;h3&gt;What This Costs in Practice&lt;/h3&gt;
&lt;p&gt;The abstract benchmark numbers translate into concrete time and electricity costs when you are generating audio for a library of blog posts.&lt;/p&gt;
&lt;p&gt;A typical TinyComputers post runs 3,000 to 5,000 words, producing 15 to 25 minutes of narrated audio. At steady-state RTF:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Machine&lt;/th&gt;
&lt;th&gt;15 min audio&lt;/th&gt;
&lt;th&gt;25 min audio&lt;/th&gt;
&lt;th&gt;System Power&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;M3 Max&lt;/td&gt;
&lt;td&gt;~38 min&lt;/td&gt;
&lt;td&gt;~63 min&lt;/td&gt;
&lt;td&gt;~50W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Radeon 8060S&lt;/td&gt;
&lt;td&gt;~38 min&lt;/td&gt;
&lt;td&gt;~63 min&lt;/td&gt;
&lt;td&gt;~100W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tesla P40&lt;/td&gt;
&lt;td&gt;~110 min&lt;/td&gt;
&lt;td&gt;~183 min&lt;/td&gt;
&lt;td&gt;~400W&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The M3 Max and Radeon 8060S are tied on generation time, but the M3 Max draws roughly half the system power. For a single post, the electricity cost difference is negligible, a fraction of a cent. For batch processing a backlog of thirty posts, the M3 Max costs about \$0.18 in electricity versus \$0.36 for the AMD and \$3.50 for the P40.&lt;/p&gt;
&lt;p&gt;None of these numbers are alarming. Even the P40, at nearly two and a half hours per post and 400 watts from the wall, costs under fifteen cents in electricity per narration at Minnesota residential rates. The equivalent Google Cloud TTS job would cost \$4 to \$16 per post depending on the voice quality tier.&lt;/p&gt;
&lt;p&gt;To put cloud costs in perspective: I recently ran a fiction novel through Google's Chirp3-HD voice: 82,000 words, roughly 500,000 characters of text plus SSML markup. The bill came to \$17.25 at Google's rate of \$30 per million characters. That is not unreasonable for a one-off project, but it adds up quickly if you are generating audio regularly. The entire library of TinyComputers narrations (dozens of posts, hours of audio) has cost me nothing beyond the electricity to run the machines I already own. The economics of local TTS are favorable on every machine in the comparison.&lt;/p&gt;
&lt;p&gt;The real cost is time. If I am generating audio for a single new post, I start it on whichever machine is idle and check back in an hour. If I am regenerating audio for twenty posts after changing the speaker voice or updating the pipeline, the M3 Max or AMD will finish overnight. The P40 would take most of a weekend.&lt;/p&gt;
&lt;h3&gt;The Right Machine for the Job&lt;/h3&gt;
&lt;p&gt;After running these benchmarks, my workflow has shifted. The M3 Max is the default for new post narration; it is fast, quiet, and I am usually sitting in front of it when I finish writing. The AMD handles batch jobs and overnight processing, where its slightly higher power draw does not matter and its equivalent speed makes it interchangeable with the Mac. The P40 server is reserved for what it does best: &lt;a href="https://tinycomputers.io/posts/repurposing-enterprise-gpus-the-tesla-p40-home-lab-story.html"&gt;running large language models&lt;/a&gt; through Ollama, where its 96GB of aggregate VRAM gives it an advantage that neither the Mac nor the AMD can match.&lt;/p&gt;
&lt;p&gt;The P40 can still generate TTS in a pinch, and it does; when both other machines are occupied, I will queue a job on the P40 and accept the longer wait. But for a workload that is inherently autoregressive, memory-bandwidth sensitive, and dependent on bf16 precision, a ten-year-old Pascal GPU is the wrong tool.&lt;/p&gt;
&lt;p&gt;What surprised me most is how well the AMD performs. The Radeon 8060S is an integrated GPU sharing system memory with the CPU. It has no HBM, no dedicated VRAM, no NVLink. Its ROCm software stack requires environment variable hacks, pre-release PyTorch wheels, and a GFX version override to function at all. And yet, once the kernels warm up, it matches Apple's best laptop silicon stride for stride. The raw hardware is there: 40 RDNA 3.5 compute units with access to a deep pool of DDR5 memory. The software just needs to get out of the way, and on run 2 and beyond, it does.&lt;/p&gt;
&lt;h3&gt;Lessons&lt;/h3&gt;
&lt;p&gt;Three takeaways from this exercise that generalize beyond TTS:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Short benchmarks lie.&lt;/strong&gt; Kernel compilation overhead on MPS and ROCm is large enough to dominate a short test. If you are evaluating a new model on non-CUDA hardware, run it at least twice before drawing conclusions. The first run is measuring the software stack, not the hardware.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Architecture matters more than clock speed.&lt;/strong&gt; The P40 has more raw FLOPS than the Radeon 8060S. It does not matter. The P40 lacks native bf16, lacks efficient attention primitives, and sits behind a PCIe 3.0 bus. The Radeon has all three, and ties a chip designed by Apple's custom silicon team. For autoregressive models, the architectural fit between model and hardware dominates everything else.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Model choice can outweigh hardware choice.&lt;/strong&gt; F5-TTS running on the weakest GPU in this comparison is five times faster than Qwen3-TTS running on the strongest. If your constraint is generation speed and you can accept a modest quality trade-off, switching to a flow-matching architecture gains more than any hardware upgrade short of a data center GPU.&lt;/p&gt;
&lt;p&gt;The audio player at the top of each post on this site represents a few minutes of machine time on one of these three machines. Which machine generated it depends on the day, the workload, and what else is running. The listener cannot tell the difference. The audio sounds the same regardless of whether it was generated on a laptop, a mini desktop, or a rack-mount server in a cold Minnesota shop. That is the real benchmark: not which machine is fastest, but that all three are fast enough.&lt;/p&gt;</description><category>amd</category><category>apple silicon</category><category>audio</category><category>benchmarks</category><category>cuda</category><category>gpu</category><category>inference</category><category>m3 max</category><category>machine learning</category><category>mps</category><category>nvidia</category><category>qwen</category><category>rocm</category><category>strix halo</category><category>tesla p40</category><category>text-to-speech</category><category>tts</category><guid>https://tinycomputers.io/posts/the-real-cost-of-running-qwen-tts-locally-three-machines-compared.html</guid><pubDate>Thu, 12 Mar 2026 14:00:00 GMT</pubDate></item><item><title>Upgrading ROCm 7.0 to 7.2 on AMD Strix Halo (gfx1151)</title><link>https://tinycomputers.io/posts/upgrading-rocm-7.0-to-7.2-on-amd-strix-halo-gfx1151.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/upgrading-rocm-7.0-to-7.2-on-amd-strix-halo-gfx1151_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;15 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;Introduction&lt;/h3&gt;
&lt;p&gt;If you're running AMD's Strix Halo hardware -- specifically the Ryzen AI MAX+ 395 with its integrated Radeon 8060S GPU -- you already know the software ecosystem is a moving target. The gfx1151 architecture sits in an awkward spot: powerful hardware that isn't officially listed on AMD's ROCm support matrix, yet functional enough to run real workloads with the right driver stack. When ROCm 7.2 landed in early 2026, upgrading from 7.0.2 was a priority. The newer stack brings an updated HSA runtime, a refreshed amdgpu kernel module, and broader compatibility improvements that matter on bleeding-edge silicon.&lt;/p&gt;
&lt;p&gt;This post documents the complete upgrade procedure from ROCm 7.0.2 to 7.2 on a production Ubuntu 24.04 system. It's not a theoretical exercise -- this was performed on a live server running QEMU virtual machines and network services, with the expectation that everything would come back online after a single reboot.&lt;/p&gt;
&lt;p&gt;AMD's official documentation states that in-place ROCm upgrades are not supported. The recommended path is a full uninstall followed by a clean reinstall. That's exactly what we did, and the entire process took about 20 minutes of wall-clock time (excluding the reboot).&lt;/p&gt;
&lt;h3&gt;System Overview&lt;/h3&gt;
&lt;p&gt;The target system is a &lt;a href="https://baud.rs/WZgnl1"&gt;Bosgame mini PC&lt;/a&gt; running the Ryzen AI MAX+ 395 APU. If you've read the &lt;a href="https://tinycomputers.io/posts/amd-ai-max+-395-system-review-a-comprehensive-analysis/"&gt;earlier review&lt;/a&gt; of this hardware, you'll be familiar with the specs. For context on this upgrade, here's what matters:&lt;/p&gt;
&lt;h4&gt;Hardware&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU&lt;/strong&gt;: AMD Ryzen AI MAX+ 395, 16 cores / 32 threads, Zen 5&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU&lt;/strong&gt;: Integrated Radeon 8060S, 40 Compute Units, RDNA 3.5 (gfx1151)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory&lt;/strong&gt;: 32 GB DDR5, unified architecture with 96 GB allocatable to GPU&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Peak GPU Clock&lt;/strong&gt;: 2,900 MHz&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Software (Pre-Upgrade)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OS&lt;/strong&gt;: Ubuntu 24.04.3 LTS (Noble Numbat)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kernel&lt;/strong&gt;: 6.14.0-37-generic (HWE, pinned)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROCm&lt;/strong&gt;: 7.0.2&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;amdgpu-dkms&lt;/strong&gt;: 6.14.14 (from &lt;code&gt;repo.radeon.com/amdgpu/30.10.2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROCk Module&lt;/strong&gt;: 6.14.14&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Running Services&lt;/h4&gt;
&lt;p&gt;The system was actively serving several roles during the upgrade:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Five QEMU virtual machines (three x86, two aarch64)&lt;/li&gt;
&lt;li&gt;A PXE boot server (dnsmasq) for the local network&lt;/li&gt;
&lt;li&gt;Docker daemon with various containers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these services are tied to the GPU driver stack, so the plan was to perform the upgrade and reboot without shutting them down first. The VMs and network services would come back automatically after the reboot.&lt;/p&gt;
&lt;h3&gt;Why Upgrade&lt;/h3&gt;
&lt;p&gt;ROCm 7.0.2 worked on this hardware. Models loaded, inference ran, &lt;code&gt;rocminfo&lt;/code&gt; detected the GPU. So why bother upgrading?&lt;/p&gt;
&lt;p&gt;Three reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Driver maturity for gfx1151&lt;/strong&gt;: The amdgpu kernel module jumped from 6.14.14 to 6.16.13 between the two releases. That's not a minor revision -- it represents months of kernel driver development. On hardware that isn't officially supported, newer drivers tend to bring meaningful stability improvements as AMD's internal teams encounter and fix issues on adjacent architectures.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HSA Runtime improvements&lt;/strong&gt;: ROCm 7.2 ships HSA Runtime Extension version 1.15, up from 1.11 in ROCm 7.0.2. The HSA (Heterogeneous System Architecture) runtime is the lowest layer of the ROCm software stack -- it handles device discovery, memory management, and kernel dispatch. Improvements here affect everything built on top of it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ecosystem alignment&lt;/strong&gt;: PyTorch wheels, Ollama builds, and other ROCm-dependent tools increasingly target 7.2 as the baseline. Running 7.0.2 was becoming an exercise in version pinning and compatibility workarounds.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;The Kernel Hold: Why It Matters&lt;/h3&gt;
&lt;p&gt;Before diving into the procedure, a note on kernel management. This system runs the Ubuntu HWE (Hardware Enablement) kernel, which provides newer kernel versions on LTS releases. At the time of this upgrade, the HWE kernel was 6.14.0-37-generic. The upstream kernel had already moved to 6.17, but we didn't want the ROCm upgrade to pull in a kernel that AMD's DKMS module might not build against.&lt;/p&gt;
&lt;p&gt;The solution is &lt;code&gt;apt-mark hold&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt-mark&lt;span class="w"&gt; &lt;/span&gt;hold&lt;span class="w"&gt; &lt;/span&gt;linux-generic-hwe-24.04&lt;span class="w"&gt; &lt;/span&gt;linux-headers-generic-hwe-24.04&lt;span class="w"&gt; &lt;/span&gt;linux-image-generic-hwe-24.04
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This prevents &lt;code&gt;apt&lt;/code&gt; from upgrading the kernel meta-packages, effectively pinning the system to 6.14.0-37-generic. The hold was already in place before the upgrade and remained untouched throughout. After the upgrade, we confirmed it was still active:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;apt-mark&lt;span class="w"&gt; &lt;/span&gt;showhold
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;linux-generic-hwe-24.04
linux-headers-generic-hwe-24.04
linux-image-generic-hwe-24.04
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you're running Strix Halo or any other hardware where kernel compatibility with &lt;code&gt;amdgpu-dkms&lt;/code&gt; is uncertain, kernel holds are essential. A kernel upgrade that breaks the DKMS build means no GPU driver after reboot.&lt;/p&gt;
&lt;h3&gt;Upgrade Procedure&lt;/h3&gt;
&lt;h4&gt;Step 1: Uninstall the Current ROCm Stack&lt;/h4&gt;
&lt;p&gt;AMD provides the &lt;code&gt;amdgpu-uninstall&lt;/code&gt; script for exactly this purpose. It removes all ROCm userspace packages and the amdgpu-dkms kernel module in a single operation:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;amdgpu-uninstall&lt;span class="w"&gt; &lt;/span&gt;-y
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This command removed approximately 120 packages, including the full HIP runtime, rocBLAS, MIOpen, MIGraphX, ROCm SMI, the LLVM-based compiler toolchain, and the Mesa graphics drivers that ship with ROCm. The DKMS module was purged, which means the amdgpu kernel module was removed from the 6.14.0-37-generic kernel's module tree.&lt;/p&gt;
&lt;p&gt;After the ROCm stack was removed, we purged the &lt;code&gt;amdgpu-install&lt;/code&gt; meta-package itself:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;purge&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;amdgpu-install
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This also cleaned up the APT repository entries that &lt;code&gt;amdgpu-install&lt;/code&gt; had configured in &lt;code&gt;/etc/apt/sources.list.d/&lt;/code&gt;. The old repos -- &lt;code&gt;repo.radeon.com/amdgpu/30.10.2&lt;/code&gt;, &lt;code&gt;repo.radeon.com/rocm/apt/7.0.2&lt;/code&gt;, and &lt;code&gt;repo.radeon.com/graphics/7.0.2&lt;/code&gt; -- were all removed automatically.&lt;/p&gt;
&lt;h4&gt;Step 2: Clean Up Leftover Files&lt;/h4&gt;
&lt;p&gt;The package removal was thorough but not perfect. A few leftover directories remained in &lt;code&gt;/opt/&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;ls&lt;span class="w"&gt; &lt;/span&gt;/opt/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;rocm
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;rocm-7.0.0
rocm-7.0.2
rocm-7.9.0
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;rocm-7.0.0&lt;/code&gt; directory was from a previous installation attempt. The &lt;code&gt;rocm-7.9.0&lt;/code&gt; was from an earlier experiment with a release candidate build. The &lt;code&gt;rocm-7.0.2&lt;/code&gt; directory contained a single orphaned shared library (&lt;code&gt;libamdhip64.so.6&lt;/code&gt;) that dpkg couldn't remove because the directory wasn't empty. All three were cleaned up manually:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;/opt/rocm-7.0.0&lt;span class="w"&gt; &lt;/span&gt;/opt/rocm-7.0.2&lt;span class="w"&gt; &lt;/span&gt;/opt/rocm-7.9.0
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It's worth checking for stale ROCm directories after any uninstall. They consume negligible disk space but can confuse build systems and scripts that scan &lt;code&gt;/opt/rocm*&lt;/code&gt; for active installations.&lt;/p&gt;
&lt;h4&gt;Step 3: Install the ROCm 7.2 Installer&lt;/h4&gt;
&lt;p&gt;AMD distributes ROCm through a meta-package called &lt;code&gt;amdgpu-install&lt;/code&gt;. Each ROCm release has its own version of this package, which configures the appropriate APT repositories. The 7.2 installer was downloaded directly from AMD's repository:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/tmp
wget&lt;span class="w"&gt; &lt;/span&gt;https://repo.radeon.com/amdgpu-install/7.2/ubuntu/noble/amdgpu-install_7.2.70200-1_all.deb
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;./amdgpu-install_7.2.70200-1_all.deb
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;update
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After installation and &lt;code&gt;apt update&lt;/code&gt;, three new repositories were active:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://repo.radeon.com/amdgpu/30.30/ubuntu noble&lt;/code&gt; -- the kernel driver and Mesa components&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://repo.radeon.com/rocm/apt/7.2 noble&lt;/code&gt; -- the ROCm userspace stack&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://repo.radeon.com/graphics/7.2/ubuntu noble&lt;/code&gt; -- graphics libraries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The version numbering can be confusing. The &lt;code&gt;amdgpu-install&lt;/code&gt; package version is &lt;code&gt;30.30.0.0.30300000-2278356.24.04&lt;/code&gt;, which maps to the amdgpu driver release 30.30. The ROCm version is 7.2.0. These are different version tracks that AMD maintains in parallel.&lt;/p&gt;
&lt;h4&gt;Step 4: Install ROCm 7.2&lt;/h4&gt;
&lt;p&gt;With the repositories configured, the actual installation was a single command:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;amdgpu-install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;--usecase&lt;span class="o"&gt;=&lt;/span&gt;graphics,rocm
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;--usecase=graphics,rocm&lt;/code&gt; flag tells the installer to include both the Mesa graphics drivers and the full ROCm compute stack. This is the right choice for a system that needs both display output and GPU compute capabilities.&lt;/p&gt;
&lt;p&gt;The installation took approximately 10 minutes and included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;amdgpu-dkms 6.16.13&lt;/strong&gt;: The kernel module, compiled via DKMS against the running kernel&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full ROCm 7.2 stack&lt;/strong&gt;: HIP runtime, hipcc compiler, rocBLAS, rocFFT, MIOpen, MIGraphX, RCCL, ROCm SMI, ROCProfiler, and dozens of other libraries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mesa graphics&lt;/strong&gt;: Updated EGL, OpenGL, and Vulkan drivers from the amdgpu Mesa fork&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROCm LLVM toolchain&lt;/strong&gt;: The LLVM-based compiler infrastructure that HIP uses for kernel compilation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The DKMS build is the critical step. During installation, DKMS compiled the amdgpu module against the kernel headers for 6.14.0-37-generic. The output confirmed a successful build:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;depmod...
update-initramfs: Generating /boot/initrd.img-6.14.0-37-generic
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The initramfs was regenerated to include the new module, ensuring it would be loaded at boot.&lt;/p&gt;
&lt;h4&gt;Step 5: Verify DKMS&lt;/h4&gt;
&lt;p&gt;Before rebooting, we confirmed the DKMS status:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;dkms&lt;span class="w"&gt; &lt;/span&gt;status
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;amdgpu/6.16.13-2278356.24.04, 6.14.0-37-generic, x86_64: installed
virtualbox/7.0.16, 6.14.0-36-generic, x86_64: installed
virtualbox/7.0.16, 6.14.0-37-generic, x86_64: installed
virtualbox/7.0.16, 6.8.0-100-generic, x86_64: installed
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The new amdgpu module (6.16.13) was built and installed for 6.14.0-37-generic. Note that it only built for the currently running kernel, unlike VirtualBox which had modules built for older kernels as well. This is expected -- DKMS builds against available kernel headers, and the old kernel headers for 6.14.0-36 and 6.8.0-100 were still present from earlier installations.&lt;/p&gt;
&lt;h4&gt;Step 6: Reboot&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;reboot
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The server came back online in approximately 50 seconds.&lt;/p&gt;
&lt;h3&gt;Post-Reboot Verification&lt;/h3&gt;
&lt;h4&gt;rocminfo&lt;/h4&gt;
&lt;p&gt;The first check after reboot was &lt;code&gt;rocminfo&lt;/code&gt;, which queries the HSA runtime for available agents:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;rocminfo
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;ROCk&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;6.16&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loaded&lt;/span&gt;
&lt;span class="o"&gt;=====================&lt;/span&gt;
&lt;span class="n"&gt;HSA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;
&lt;span class="o"&gt;=====================&lt;/span&gt;
&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="mf"&gt;1.18&lt;/span&gt;
&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Ext&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="mf"&gt;1.15&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;==========&lt;/span&gt;
&lt;span class="n"&gt;HSA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Agents&lt;/span&gt;
&lt;span class="o"&gt;==========&lt;/span&gt;
&lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AMD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RYZEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MAX&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;395&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Radeon&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8060&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CPU&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gfx1151&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GPU&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Marketing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="n"&gt;AMD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Radeon&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Graphics&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Compute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Max&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Clock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Freq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MHz&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;2900&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Memory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="n"&gt;APU&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ISA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amdgcn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;amd&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;amdhsa&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;gfx1151&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ISA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amdgcn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;amd&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;amdhsa&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;gfx11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;generic&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Key observations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ROCk module 6.16.13&lt;/strong&gt;: The new kernel module loaded successfully.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime Ext Version 1.15&lt;/strong&gt;: Upgraded from 1.11 in ROCm 7.0.2.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;gfx1151 detected&lt;/strong&gt;: The GPU was recognized with its correct ISA identifier.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;gfx11-generic ISA&lt;/strong&gt;: ROCm 7.2 also exposes a generic gfx11 ISA, which allows software compiled for the broader RDNA 3 family to run on this device without gfx1151-specific builds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;APU memory&lt;/strong&gt;: The memory properties correctly identify this as an APU with unified memory.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;ROCm SMI&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;rocm-smi
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Device  Node  Temp    Power     SCLK  MCLK     Fan  Perf  VRAM%  GPU%
0       1     33.0C   9.087W    N/A   1000Mhz  0%   auto  0%     0%
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The GPU was visible and reporting telemetry. The 0% VRAM reading is expected on an APU -- &lt;code&gt;rocm-smi&lt;/code&gt; reports dedicated VRAM usage, but on a unified memory architecture, GPU memory allocations come from system RAM and aren't reflected in this counter.&lt;/p&gt;
&lt;h4&gt;ROCm Version&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;cat&lt;span class="w"&gt; &lt;/span&gt;/opt/rocm/.info/version
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="mf"&gt;7.2.0&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;DKMS&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;dkms&lt;span class="w"&gt; &lt;/span&gt;status
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Confirmed &lt;code&gt;amdgpu/6.16.13&lt;/code&gt; remained installed for 6.14.0-37-generic after reboot.&lt;/p&gt;
&lt;h3&gt;PyTorch Validation&lt;/h3&gt;
&lt;p&gt;With the driver stack verified, the next step was confirming that PyTorch could see and use the GPU. ROCm 7.2 ships with prebuilt PyTorch wheels on AMD's repository.&lt;/p&gt;
&lt;h4&gt;Installing PyTorch for ROCm 7.2&lt;/h4&gt;
&lt;p&gt;We set up a Python virtual environment and installed the ROCm-specific wheels:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;python3&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;venv&lt;span class="w"&gt; &lt;/span&gt;.venv
&lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.venv/bin/activate
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;--upgrade&lt;span class="w"&gt; &lt;/span&gt;pip
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The PyTorch wheel for ROCm 7.2 requires a matching ROCm-specific build of Triton. Both are available from AMD's manylinux repository. The order matters -- Triton must be installed first, since the PyTorch wheel declares it as a dependency with a specific version that doesn't exist on PyPI:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/triton-3.5.1%2Brocm7.2.0.gita272dfa8-cp312-cp312-linux_x86_64.whl
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torch-2.9.1%2Brocm7.2.0.lw.git7e1940d4-cp312-cp312-linux_x86_64.whl
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchvision-0.24.0%2Brocm7.2.0.gitb919bd0c-cp312-cp312-linux_x86_64.whl
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These are the ROCm 7.2 builds for Python 3.12. AMD also provides wheels for Python 3.10, 3.11, and 3.13.&lt;/p&gt;
&lt;h4&gt;Smoke Test&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"PyTorch:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__version__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"CUDA available:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Device:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_device_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"VRAM:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_device_properties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_memory&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1e9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"GB"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;PyTorch&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.9&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;rocm7&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;git7e1940d4&lt;/span&gt;
&lt;span class="n"&gt;CUDA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;Device&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AMD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Radeon&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Graphics&lt;/span&gt;
&lt;span class="n"&gt;VRAM&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;103.1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GB&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;PyTorch detected the GPU through ROCm's HIP-to-CUDA translation layer. The 103.1 GB figure represents the total addressable memory on this unified-memory APU, which includes both the 96 GB GPU allocation and additional system memory accessible through the HSA runtime.&lt;/p&gt;
&lt;p&gt;Note the use of &lt;code&gt;torch.cuda&lt;/code&gt; despite this being an AMD GPU. ROCm's HIP runtime presents itself through PyTorch's CUDA interface, so all CUDA API calls in PyTorch (device selection, memory management, kernel launches) work transparently with AMD hardware.&lt;/p&gt;
&lt;h3&gt;Before and After Summary&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;ROCm 7.0.2&lt;/th&gt;
&lt;th&gt;ROCm 7.2.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ROCm Version&lt;/td&gt;
&lt;td&gt;7.0.2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.2.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;amdgpu-dkms&lt;/td&gt;
&lt;td&gt;6.14.14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.16.13&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ROCk Module&lt;/td&gt;
&lt;td&gt;6.14.14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.16.13&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HSA Runtime Ext&lt;/td&gt;
&lt;td&gt;1.11&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.15&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;amdgpu Repo&lt;/td&gt;
&lt;td&gt;30.10.2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30.30&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;2.9.1+rocm7.2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Triton&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;3.5.1+rocm7.2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel&lt;/td&gt;
&lt;td&gt;6.14.0-37-generic&lt;/td&gt;
&lt;td&gt;6.14.0-37-generic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel Holds&lt;/td&gt;
&lt;td&gt;In place&lt;/td&gt;
&lt;td&gt;In place&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Notes on gfx1151 Support&lt;/h3&gt;
&lt;p&gt;It's worth being explicit about the support situation. As of February 2026, gfx1151 (Strix Halo) is &lt;strong&gt;not listed&lt;/strong&gt; on AMD's official ROCm support matrix. The supported RDNA 3 targets are gfx1100 (Navi 31, RX 7900 XTX) and gfx1101 (Navi 32). Strix Halo's gfx1151 is an RDNA 3.5 derivative that shares much of the ISA with gfx1100 but has architectural differences in the memory subsystem and compute unit layout.&lt;/p&gt;
&lt;p&gt;In practice, ROCm 7.2 works on gfx1151. The kernel driver loads, &lt;code&gt;rocminfo&lt;/code&gt; detects the GPU, and PyTorch can allocate tensors and dispatch compute kernels. The &lt;code&gt;gfx11-generic&lt;/code&gt; ISA target in ROCm 7.2 is particularly helpful -- it provides a compatibility path for software that hasn't been explicitly compiled for gfx1151.&lt;/p&gt;
&lt;p&gt;However, "works" and "fully supported" are different things. There are known quirks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;rocm-smi VRAM reporting&lt;/strong&gt;: Always shows 0% on the APU since it only tracks discrete VRAM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No official PyTorch gfx1151 builds&lt;/strong&gt;: The ROCm PyTorch wheels target gfx1100. They run on gfx1151 through ISA compatibility, but performance may not be optimal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Large model loading latency&lt;/strong&gt;: Moving large models to the GPU device can be slow on the unified memory architecture, as the HSA runtime handles page migration differently than discrete GPU DMA transfers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're considering this hardware for production AI workloads, treat ROCm support as "functional but experimental." It works well enough for development, testing, and moderate inference workloads. For production training or latency-sensitive deployment, stick with hardware on AMD's official support list.&lt;/p&gt;
&lt;h3&gt;Rollback Plan&lt;/h3&gt;
&lt;p&gt;If the upgrade fails -- the DKMS module doesn't build, the GPU isn't detected after reboot, or something else goes wrong -- the rollback path is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Uninstall ROCm 7.2:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;amdgpu-uninstall&lt;span class="w"&gt; &lt;/span&gt;-y
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;purge&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;amdgpu-install
&lt;/pre&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Reinstall ROCm 7.0.2:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;wget&lt;span class="w"&gt; &lt;/span&gt;https://repo.radeon.com/amdgpu-install/30.10.2/ubuntu/noble/amdgpu-install_30.10.2.0.30100200-2226257.24.04_all.deb
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;./amdgpu-install_30.10.2.0.30100200-2226257.24.04_all.deb
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;update
sudo&lt;span class="w"&gt; &lt;/span&gt;amdgpu-install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;--usecase&lt;span class="o"&gt;=&lt;/span&gt;graphics,rocm
sudo&lt;span class="w"&gt; &lt;/span&gt;reboot
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The entire rollback takes about 15 minutes. Keep the old &lt;code&gt;amdgpu-install&lt;/code&gt; deb URL handy -- it's not linked from AMD's current download pages once a newer version is published.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Upgrading ROCm on hardware that isn't officially supported always carries some risk, but this upgrade from 7.0.2 to 7.2 on gfx1151 was uneventful. The procedure follows AMD's documented uninstall-reinstall approach with no deviations. The kernel hold strategy kept the kernel stable, the DKMS module built cleanly against 6.14.0-37-generic, and all post-reboot checks passed.&lt;/p&gt;
&lt;p&gt;The improvements in ROCm 7.2 -- particularly the HSA runtime bump to 1.15 and the introduction of the &lt;code&gt;gfx11-generic&lt;/code&gt; ISA target -- represent meaningful progress for Strix Halo users. The ecosystem is slowly catching up to the hardware. It's not there yet, but each release closes the gap.&lt;/p&gt;
&lt;p&gt;For anyone running a Ryzen AI MAX+ 395 or similar Strix Halo hardware on Ubuntu 24.04, this upgrade is worth doing. The procedure is well-defined, the rollback path is clear, and the newer driver stack brings tangible benefits. Just remember to hold your kernel first.&lt;/p&gt;
&lt;h3&gt;Recommended Resources&lt;/h3&gt;
&lt;h4&gt;Hardware&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/WZgnl1"&gt;Bosgame M5 AI Mini PC (Ryzen AI MAX+ 395)&lt;/a&gt; - The system used in this post&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/q87EAZ"&gt;GMKtec EVO X2 (Ryzen AI MAX+ 395)&lt;/a&gt; - Another Strix Halo mini PC option on Amazon&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Books&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/NTAPGg"&gt;&lt;em&gt;Deep Learning with PyTorch&lt;/em&gt;&lt;/a&gt; by Stevens, Antiga, Huang, Viehmann - Comprehensive guide to building, training, and tuning neural networks with PyTorch&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/Iu8KR4"&gt;&lt;em&gt;Programming PyTorch for Deep Learning&lt;/em&gt;&lt;/a&gt; by Ian Pointer - Practical guide to creating and deploying deep learning applications&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/zmKSQj"&gt;&lt;em&gt;Understanding Deep Learning&lt;/em&gt;&lt;/a&gt; by Simon Prince - Modern treatment of deep learning fundamentals&lt;/li&gt;
&lt;/ul&gt;</description><category>amd</category><category>amdgpu</category><category>dkms</category><category>driver upgrade</category><category>gfx1151</category><category>gpu computing</category><category>linux</category><category>pytorch</category><category>rocm</category><category>ryzen ai</category><category>strix halo</category><category>ubuntu</category><guid>https://tinycomputers.io/posts/upgrading-rocm-7.0-to-7.2-on-amd-strix-halo-gfx1151.html</guid><pubDate>Wed, 18 Feb 2026 16:00:00 GMT</pubDate></item><item><title>Image Editing on 10-Year-Old GPUs: NVIDIA P40 vs AMD Strix Halo</title><link>https://tinycomputers.io/posts/image-editing-on-10-year-old-gpus-nvidia-p40-vs-amd-strix-halo.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/image-editing-on-10-year-old-gpus-nvidia-p40-vs-amd-strix-halo_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;20 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;Introduction&lt;/h3&gt;
&lt;p&gt;There's a certain satisfaction in making old hardware do new tricks. When NVIDIA released the Tesla P40 in 2016, deep learning was still finding its footing. ImageNet classification was the benchmark everyone cared about, GANs were generating blurry faces, and the idea of a 57-billion-parameter image editing model would have seemed like science fiction.&lt;/p&gt;
&lt;p&gt;Around the middle of 2017, when the P40 would have been seeing peak adoption in datacenters, I found myself in an advanced pattern recognition course, my final credits needed for a masters in computer science (the name hadn't been updated to reflect more contemporary terminology like "machine learning," let alone "deep learning"). The textbook was Bishop's &lt;a href="https://baud.rs/pme3zz"&gt;&lt;em&gt;Pattern Recognition and Machine Learning&lt;/em&gt;&lt;/a&gt;, a book that managed to make Bayesian inference feel both rigorous and approachable. We spent the last two weeks of the course looking at deep learning using TensorFlow, but we didn't even have GPU infrastructure available. Everything ran on CPU. It would have been great to have experienced the P40 in its prime, when 24 GB of VRAM and 3,840 CUDA cores made it one of the most capable inference GPUs money could buy. Instead, I'm getting acquainted with it a decade later, asking it to do things its designers never imagined.&lt;/p&gt;
&lt;p&gt;Fast forward to 2026, and here I am, running a 57-billion-parameter model on four of these decade-old GPUs, and comparing the results against AMD's latest Strix Halo APU, a chip that didn't exist until 2025.&lt;/p&gt;
&lt;p&gt;The model in question is &lt;a href="https://baud.rs/W8MlgE"&gt;FireRed-Image-Edit-1.0&lt;/a&gt; from FireRedTeam, a 57.7GB diffusion model built on the QwenImageEditPlusPipeline architecture. It takes an input image and a text prompt, then produces an edited version. The kind of thing that would have required a massive cloud GPU a couple of years ago.&lt;/p&gt;
&lt;p&gt;This post documents the full journey: the precision pitfalls of running modern diffusion models on Pascal-era GPUs, the quantization trade-offs that make or break image quality, and the head-to-head performance comparison that produced some genuinely surprising results. All of the inference scripts and output images are available on &lt;a href="https://baud.rs/V3qpTJ"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;The Hardware&lt;/h3&gt;
&lt;h4&gt;NVIDIA Tesla P40 (2016)&lt;/h4&gt;
&lt;p&gt;The P40 was NVIDIA's inference-focused datacenter GPU from the Pascal generation. The key specs for our purposes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Architecture&lt;/strong&gt;: Pascal (sm_6.1)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CUDA Cores&lt;/strong&gt;: 3,840&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory&lt;/strong&gt;: 24 GB GDDR5X&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Bandwidth&lt;/strong&gt;: 346 GB/s&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FP32 Performance&lt;/strong&gt;: 12 TFLOPS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FP16 Performance&lt;/strong&gt;: Limited, no native FP16 tensor cores&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BF16 Support&lt;/strong&gt;: None&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Price today&lt;/strong&gt;: &lt;a href="https://baud.rs/QaDJDo"&gt;~$100-200 per card on the secondary market&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I have four of these cards in a server, giving me 96 GB of total VRAM, but spread across four separate memory spaces, which introduces its own challenges.&lt;/p&gt;
&lt;h4&gt;AMD Ryzen AI MAX+ 395 / Strix Halo (2025)&lt;/h4&gt;
&lt;p&gt;AMD's Strix Halo is a different beast entirely. It's an APU (CPU and GPU on the same die, sharing the same memory pool):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPU Architecture&lt;/strong&gt;: RDNA 3.5 (gfx1151)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute Units&lt;/strong&gt;: 40 CUs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory&lt;/strong&gt;: 128 GB unified LPDDR5X (32 GB for CPU, 96 GB for VRAM)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Bandwidth&lt;/strong&gt;: ~256 GB/s (shared)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BF16 Support&lt;/strong&gt;: Yes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FP16 Support&lt;/strong&gt;: Yes (Fast F16 Operation)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROCm&lt;/strong&gt;: 7.9.0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Price&lt;/strong&gt;: &lt;a href="https://baud.rs/q87EAZ"&gt;~$2,000+ for the complete system&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The unified memory architecture means all 96 GB is accessible to the GPU without any PCIe transfer overhead, and the entire model can live in a single memory space.&lt;/p&gt;
&lt;h3&gt;The Model: FireRed-Image-Edit-1.0&lt;/h3&gt;
&lt;p&gt;FireRed-Image-Edit is a diffusion-based image editing model with three major components:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transformer&lt;/td&gt;
&lt;td&gt;40.9 GB&lt;/td&gt;
&lt;td&gt;QwenImageTransformer2DModel, 60 layers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text Encoder&lt;/td&gt;
&lt;td&gt;16.6 GB&lt;/td&gt;
&lt;td&gt;Qwen2.5-VL 7B vision-language model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VAE&lt;/td&gt;
&lt;td&gt;~0.3 GB&lt;/td&gt;
&lt;td&gt;AutoencoderKL for encoding/decoding images&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Total: &lt;strong&gt;57.7 GB&lt;/strong&gt; of model weights. The scheduler is FlowMatchEulerDiscreteScheduler, and the pipeline uses true classifier-free guidance (CFG), which roughly doubles the memory needed during inference since it runs both conditional and unconditional passes.&lt;/p&gt;
&lt;p&gt;The test task: take this input image and apply the prompt &lt;em&gt;"Add a red hat on the cat"&lt;/em&gt;; the model draws a cat wearing a red hat onto the book cover, rendered in the style of the O'Reilly animal illustrations.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/firered-input.png.webp" alt="Input image, a person holding an O'Reilly Python book" style="width: 480px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: right; padding: 20px;" loading="lazy"&gt;&lt;/p&gt;
&lt;h3&gt;The P40 Challenge: When FP16 Breaks Everything&lt;/h3&gt;
&lt;h4&gt;The Precision Problem&lt;/h4&gt;
&lt;p&gt;The first, and biggest, challenge with the P40s is numerical precision. Modern diffusion models are designed for BF16 (bfloat16), which has the same exponent range as FP32 (8 exponent bits, range ±3.4×10³⁸) but with reduced mantissa precision. The P40, being a Pascal-era GPU, supports neither BF16 nor proper FP16 tensor operations.&lt;/p&gt;
&lt;p&gt;FP16 has only 5 exponent bits, giving it a range of ±65,504. This might seem sufficient, but the diffusion scheduler's internal sigma values and the VAE's convolution operations routinely produce intermediate values that overflow this range. The FlowMatchEulerDiscreteScheduler, in particular, works with sigma schedules that can produce large intermediate values during the noise prediction and scaling steps. When these overflow FP16's limited range, they become NaN or Inf, and these corrupt values propagate through every subsequent operation (matrix multiplications, attention computations, residual connections) until the entire tensor is garbage.&lt;/p&gt;
&lt;p&gt;The result: NaN propagation that silently corrupts the entire pipeline, producing an all-black output image.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/firered-p40-fp16-black.png.webp" alt="The output of FP16 inference on the P40, a completely black image from NaN corruption" style="width: 480px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px;" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;This was the most time-consuming discovery in the entire project. The model would load, the progress bar would advance through all 40 denoising steps without any indication of trouble, and then the output would be perfectly black: &lt;code&gt;mean=0.0, min=0, max=0&lt;/code&gt;. No error messages. No warnings. No NaN detection exceptions. Just silent numerical corruption that only becomes visible when you look at the final image.&lt;/p&gt;
&lt;p&gt;The debugging process was particularly frustrating because the corruption happens gradually. Partial NaN contamination in early steps doesn't crash anything; the attention mechanisms and residual connections continue to produce tensor outputs of the expected shapes. The model appears to be working normally right up until the final image is decoded from all-zero latents.&lt;/p&gt;
&lt;h4&gt;The FP32 Solution (and a Speed Surprise)&lt;/h4&gt;
&lt;p&gt;The fix was to run the entire pipeline in FP32: scheduler, VAE, and all non-quantized transformer layers. The quantized weights themselves stay compressed (INT8 or NF4), but every arithmetic operation uses full 32-bit precision.&lt;/p&gt;
&lt;p&gt;It wasn't enough to just set the quantization compute dtype to FP32; that only fixes the dequantized matmul operations inside the quantized layers. The scheduler's sigma arithmetic, the VAE's convolution operations, and the non-quantized components (layer norms, biases, attention scaling) all needed FP32 as well. Similarly, loading the pipeline with &lt;code&gt;torch_dtype=torch.float32&lt;/code&gt; but leaving the transformer's non-quantized layers in FP16 caused a dtype mismatch in the attention mechanism; PyTorch's scaled dot-product attention requires query, key, and value tensors to share the same dtype. Every component in the computational chain needed to be FP32.&lt;/p&gt;
&lt;p&gt;The one exception is the text encoder, which runs once before the denoising loop begins. It stays in FP16 on its own GPU, and its output embeddings are upcast to FP32 when transferred to the main device. This is safe because the text encoder doesn't participate in the iterative process where precision errors compound.&lt;/p&gt;
&lt;p&gt;Here's where things got interesting: &lt;strong&gt;FP32 was actually faster than FP16 on the P40.&lt;/strong&gt; The first attempts with FP16 ran at approximately 9 minutes per denoising step. After switching to FP32, the same operations completed in about 2.4 minutes per step with NF4, and 1.5 minutes per step with INT8. The P40's FP32 throughput is its native strength; it was designed for FP32 datacenter inference, after all. FP16 on Pascal is handled through slower pathways that add overhead rather than saving it.&lt;/p&gt;
&lt;h4&gt;Multi-GPU Device Orchestration&lt;/h4&gt;
&lt;p&gt;With 57.7 GB of model weights and only 24 GB per GPU, some form of model sharding or quantization is mandatory. After extensive testing, the optimal configuration for the P40s turned out to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPU 0&lt;/strong&gt;: INT8-quantized transformer (~22 GB)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU 1&lt;/strong&gt;: Text encoder in FP16 (~16.6 GB)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU 2&lt;/strong&gt;: VAE in FP32 (~6.6 GB including decode workspace)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU 3&lt;/strong&gt;: Unused&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This layout requires significant monkey-patching of the diffusers pipeline. The &lt;code&gt;_execution_device&lt;/code&gt; property must be overridden to ensure latents are created on the correct GPU. The &lt;code&gt;encode_prompt&lt;/code&gt; method needs patching to route inputs to the text encoder's GPU and move the resulting embeddings back. And for the INT8 configuration, the VAE's encode and decode methods need wrappers to handle cross-device tensor transfers.&lt;/p&gt;
&lt;p&gt;The text encoder stays in FP16 because it fits on a single GPU and its outputs are immediately upcast to FP32 when moved to the main device. This is safe because the text encoder runs once at the beginning; it doesn't participate in the iterative denoising loop where precision matters most.&lt;/p&gt;
&lt;h4&gt;Quantization Quality: INT8 vs NF4&lt;/h4&gt;
&lt;p&gt;With the FP32 pipeline in place, I tested both INT8 (8-bit) and NF4 (4-bit) quantization for the transformer:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NF4 (4-bit quantization):&lt;/strong&gt;
The NF4 approach uses bitsandbytes' normalized float 4-bit quantization with double quantization enabled. The transformer compresses from 40.9 GB to roughly 10 GB, easily fitting on a single P40 alongside the VAE. However, the output quality was significantly degraded, with heavy noise and grain throughout the image, even at the full 40 denoising steps. Each denoising step introduces small numerical errors from the 4-bit weight approximations, and these errors compound across 40 iterations.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/firered-p40-nf4-noisy.png.webp" alt="P40 NF4 output, 4-bit quantization introduces heavy noise that compounds over 40 denoising steps" style="width: 480px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: right; padding: 20px;" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;INT8 (8-bit quantization):&lt;/strong&gt;
INT8 produced dramatically better results. The output was clean and sharp, visually comparable to what you'd expect from full-precision inference on a modern GPU. The 8-bit precision preserves enough information in the weights that the per-step errors don't accumulate into visible artifacts.&lt;/p&gt;
&lt;div style="clear: both;"&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src="https://tinycomputers.io/images/firered-p40-int8-clean.png.webp" alt="P40 INT8 output, clean and sharp, with a cat in a red hat added to the book cover" style="width: 480px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px;" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;The trade-off is memory: the INT8 transformer occupies ~22 GB, nearly filling an entire P40. This is why the VAE had to move to a third GPU; there wasn't enough headroom on GPU 0 for the VAE's convolution workspace during the decode phase. An early attempt that kept the VAE on GPU 0 ran all 40 denoising steps successfully, only to crash with an out-of-memory error at the very last operation.&lt;/p&gt;
&lt;div style="clear: both;"&gt;&lt;/div&gt;

&lt;h3&gt;The Strix Halo Experience: Simplicity Wins&lt;/h3&gt;
&lt;h4&gt;BF16 Full Precision&lt;/h4&gt;
&lt;p&gt;Running the same model on the Strix Halo was refreshingly simple. With 96 GB of unified VRAM and native BF16 support, the entire pipeline loads in a few lines:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;QwenImageEditPlusPipeline&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;torch_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cuda:0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;No quantization. No multi-GPU patching. No device transfer hooks. No FP32 workarounds. The model loads in BF16 and runs natively.&lt;/p&gt;
&lt;div style="clear: both;"&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src="https://tinycomputers.io/images/firered-strix-bf16-clean.png.webp" alt="Strix Halo BF16 output, visually identical to the P40 INT8 result" style="width: 480px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: right; padding: 20px;" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;During inference, the pipeline consumed approximately 75 GB of VRAM (the true CFG doubles the workspace requirements), well within the 96 GB budget.&lt;/p&gt;
&lt;p&gt;The first run did take about 35 minutes of JIT kernel compilation before producing any inference steps; ROCm compiles HIP kernels for the gfx1151 architecture on first use. During this phase, the GPU sits at 100% utilization with no visible progress, which can be alarming if you're not expecting it. The GPU temperature climbed from 31°C idle to 69°C, and power draw went from 9W to 119W as the compiler worked through the hundreds of unique kernel configurations needed by a 60-layer transformer. These compiled kernels are cached, so subsequent runs skip this overhead entirely.&lt;/p&gt;
&lt;div style="clear: both;"&gt;&lt;/div&gt;

&lt;h4&gt;Quantization on Strix Halo: Does It Help?&lt;/h4&gt;
&lt;p&gt;Given the surprising performance parity between the two systems at full precision, I tested whether quantization could speed up the Strix Halo by reducing memory traffic. The theory was that if the workload is memory-bandwidth-limited, smaller model weights should mean faster inference.&lt;/p&gt;
&lt;p&gt;The results were definitive:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Per-Step Time&lt;/th&gt;
&lt;th&gt;40-Step Estimate&lt;/th&gt;
&lt;th&gt;VRAM Used&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16 (full precision)&lt;/td&gt;
&lt;td&gt;82.6s&lt;/td&gt;
&lt;td&gt;55 min&lt;/td&gt;
&lt;td&gt;~75 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NF4 (4-bit)&lt;/td&gt;
&lt;td&gt;83.5s&lt;/td&gt;
&lt;td&gt;56 min&lt;/td&gt;
&lt;td&gt;~30 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INT8 (8-bit)&lt;/td&gt;
&lt;td&gt;94.9s&lt;/td&gt;
&lt;td&gt;63 min&lt;/td&gt;
&lt;td&gt;~44 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;NF4 quantization produced virtually identical speed to full BF16. The model shrank from 75 GB to 30 GB of VRAM usage, but inference time didn't improve at all. INT8 was actually &lt;em&gt;slower&lt;/em&gt;; the bitsandbytes INT8 matmul path adds dequantization overhead that more than offsets any memory bandwidth savings.&lt;/p&gt;
&lt;p&gt;This tells us something important about the Strix Halo's performance profile for this workload: &lt;strong&gt;it's compute-bound, not memory-bound.&lt;/strong&gt; The RDNA 3.5 GPU's 40 compute units are the bottleneck, not the LPDDR5X memory bandwidth. Reducing the model size doesn't help because the GPU is already busy with arithmetic, not waiting on memory.&lt;/p&gt;
&lt;p&gt;This contrasts with LLM inference workloads (text generation), where the Strix Halo's large memory pool is a genuine advantage. LLM token generation is almost entirely memory-bound, making quantization directly translate to speed improvements. Each token generation pass reads the entire model's weights but performs relatively little computation per weight. Diffusion models are the opposite: each denoising step runs a full forward pass through 60 transformer layers with dense matrix multiplications, attention computations, and residual connections. The arithmetic intensity is much higher, putting the pressure squarely on the GPU's compute units rather than its memory subsystem.&lt;/p&gt;
&lt;h3&gt;Head-to-Head: The Numbers&lt;/h3&gt;
&lt;p&gt;Here's the complete performance comparison across all tested configurations:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Per-Step&lt;/th&gt;
&lt;th&gt;40 Steps&lt;/th&gt;
&lt;th&gt;Image Quality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Strix Halo&lt;/td&gt;
&lt;td&gt;BF16 full precision&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;82.6s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;55 min&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clean, sharp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strix Halo&lt;/td&gt;
&lt;td&gt;NF4 (4-bit)&lt;/td&gt;
&lt;td&gt;83.5s&lt;/td&gt;
&lt;td&gt;56 min&lt;/td&gt;
&lt;td&gt;Clean (10-step test)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strix Halo&lt;/td&gt;
&lt;td&gt;INT8 (8-bit)&lt;/td&gt;
&lt;td&gt;94.9s&lt;/td&gt;
&lt;td&gt;63 min&lt;/td&gt;
&lt;td&gt;Clean (10-step test)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4× P40&lt;/td&gt;
&lt;td&gt;INT8 + FP32 pipeline&lt;/td&gt;
&lt;td&gt;87.5s&lt;/td&gt;
&lt;td&gt;58 min&lt;/td&gt;
&lt;td&gt;Clean, sharp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4× P40&lt;/td&gt;
&lt;td&gt;NF4 + FP32 pipeline&lt;/td&gt;
&lt;td&gt;145.9s&lt;/td&gt;
&lt;td&gt;97 min&lt;/td&gt;
&lt;td&gt;Heavy noise/grain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The headline result: &lt;strong&gt;a single AMD Strix Halo APU from 2025 is about 6% faster per step than four NVIDIA P40s from 2016 running INT8-quantized inference.&lt;/strong&gt; That's not exactly the generational leap you might expect from a decade of GPU evolution.&lt;/p&gt;
&lt;p&gt;To be fair, the comparison isn't entirely apples-to-apples. The P40 is running an 8-bit quantized model (less computation per step but with dequantization overhead), while the Strix Halo runs the full BF16 model. The P40's dedicated GDDR5X provides 346 GB/s of bandwidth to a single GPU, while the Strix Halo's LPDDR5X shares its ~256 GB/s between the CPU and GPU. And the P40 setup requires three GPUs working in concert, while the Strix Halo uses a single unified memory space.&lt;/p&gt;
&lt;h3&gt;Lessons Learned&lt;/h3&gt;
&lt;h4&gt;Old GPUs Are Surprisingly Capable&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://baud.rs/QaDJDo"&gt;Four P40s&lt;/a&gt; at ~$500 total produce inference quality and speed that's competitive with a &lt;a href="https://baud.rs/q87EAZ"&gt;$2,000+ modern APU system&lt;/a&gt;. The P40's 346 GB/s memory bandwidth per card and strong FP32 throughput remain relevant even for models that were designed for hardware two generations newer. The main challenge is software engineering: working around the precision limitations and multi-GPU complexity takes significant effort.&lt;/p&gt;
&lt;h4&gt;Precision Matters More Than Speed&lt;/h4&gt;
&lt;p&gt;The single most impactful discovery in this project was that FP16 silently corrupts diffusion model outputs on Pascal GPUs. There are no error messages, no NaN warnings during inference, just a black image at the end. The fix (using FP32 everywhere) actually improved performance, which was counterintuitive. The lesson: when dealing with older hardware, always validate your numerical precision assumptions before optimizing for speed.&lt;/p&gt;
&lt;h4&gt;Quantization Is Not Free&lt;/h4&gt;
&lt;p&gt;On the P40s, INT8 quantization was essential (the model simply wouldn't fit otherwise) and produced excellent results. NF4 was too aggressive; the 4-bit precision degraded output quality visibly.&lt;/p&gt;
&lt;p&gt;On the Strix Halo, quantization was unnecessary and even counterproductive. INT8 added overhead without any speed benefit, and NF4 didn't save time despite dramatically reducing memory usage. The takeaway: quantization's value depends entirely on your bottleneck. If you're compute-bound, smaller weights don't help.&lt;/p&gt;
&lt;h4&gt;Unified Memory Is Underrated&lt;/h4&gt;
&lt;p&gt;The Strix Halo's greatest advantage wasn't raw performance; it was simplicity. Loading a 57.7 GB model into a single 96 GB memory space eliminates an entire category of engineering problems: no device placement, no cross-GPU tensor transfers, no monkey-patching encode/decode methods, no VAE OOM surprises at the decode step. The inference script for the Strix Halo is about 50 lines. The P40 version is over 150, most of it careful device orchestration code.&lt;/p&gt;
&lt;p&gt;For anyone who values development velocity and code maintainability over squeezing the last dollar of cost-efficiency out of used datacenter hardware, unified memory APUs have a compelling argument even when they don't win on raw throughput.&lt;/p&gt;
&lt;h3&gt;What About Newer NVIDIA GPUs?&lt;/h3&gt;
&lt;p&gt;It's worth putting these numbers in context. An NVIDIA RTX 4090 with 24 GB of VRAM and native BF16/FP16 tensor core support would likely run this model (with INT8 quantization) at roughly 10-15 seconds per step, 5-8x faster than either system tested here. An A100 with 80 GB could run it unquantized in BF16 at similar or better speeds. The P40 and Strix Halo are both firmly in the "budget/accessible" tier of AI hardware.&lt;/p&gt;
&lt;p&gt;The more interesting comparison is cost-per-step. &lt;a href="https://baud.rs/QaDJDo"&gt;Four P40s from eBay&lt;/a&gt; cost about $500 total (plus a server that can host them). The &lt;a href="https://baud.rs/q87EAZ"&gt;Strix Halo system&lt;/a&gt; runs about $2,000+. Both produce essentially the same result at the same speed. The P40 route demands more engineering knowledge; the Strix Halo route demands more money.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Both systems successfully ran a 57.7 GB diffusion model that would have been considered impossibly large for consumer hardware just a few years ago. The P40s did it through clever quantization and multi-GPU orchestration. The Strix Halo did it by brute force: 96 GB of memory and native BF16 support.&lt;/p&gt;
&lt;p&gt;The performance story is more nuanced than "newer is always better." For diffusion model inference, the NVIDIA P40 (a card you can buy for $100 on eBay) remains remarkably competitive when properly configured. It requires more engineering effort, and you need to know the precision pitfalls, but the results speak for themselves.&lt;/p&gt;
&lt;p&gt;The Strix Halo's strength lies not in raw speed but in its unified memory architecture and modern instruction set support. It eliminates the multi-GPU complexity entirely, runs native BF16 without precision hacks, and provides a development experience that's orders of magnitude simpler. For iterating on models, testing new architectures, or just avoiding the headaches of cross-device tensor management, that simplicity has real value.&lt;/p&gt;
&lt;p&gt;If you're considering hardware for running large diffusion models locally, the choice comes down to how you value your time versus your budget. Four P40s and a weekend of debugging will get you to roughly the same place as a Strix Halo system that just works out of the box. Both paths lead to a cat in a red hat.&lt;/p&gt;
&lt;h3&gt;Recommended Resources&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Hardware&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/QaDJDo"&gt;NVIDIA Tesla P40 24GB&lt;/a&gt; - The GPU used in this post. Available on eBay for a fraction of the original price. You'll need a server with PCIe x16 slots and adequate cooling.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/q87EAZ"&gt;GMKtec EVO-X2 (AMD Ryzen AI MAX+ 395)&lt;/a&gt; - A compact Strix Halo mini PC with 128GB unified LPDDR5X 8000MHz, WiFi 7, and USB4. A representative platform for running large models on Strix Halo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Books&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/pme3zz"&gt;&lt;em&gt;Pattern Recognition and Machine Learning&lt;/em&gt;&lt;/a&gt; by Christopher M. Bishop - The classic that introduced many to Bayesian methods and kernel machines. Still one of the best foundations for understanding the statistical principles behind modern ML.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/dnhZCN"&gt;&lt;em&gt;Hands-On Generative AI with Transformers and Diffusion Models&lt;/em&gt;&lt;/a&gt; by Omar Sanseviero, Pedro Cuenca, Apolinário Passos, and Jonathan Whitaker - A practical guide to building and fine-tuning diffusion models using the Hugging Face ecosystem, including the diffusers library used in this post.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/vTOHER"&gt;&lt;em&gt;Understanding Deep Learning&lt;/em&gt;&lt;/a&gt; by Simon J.D. Prince - A thorough modern treatment of deep learning fundamentals through diffusion models, with excellent visualizations and mathematical rigor.&lt;/li&gt;
&lt;/ul&gt;</description><category>ai hardware</category><category>amd strix halo</category><category>benchmarks</category><category>bf16</category><category>bitsandbytes</category><category>diffusion models</category><category>firered</category><category>fp32</category><category>gfx1151</category><category>gpu computing</category><category>image generation</category><category>int8</category><category>machine learning</category><category>nf4</category><category>nvidia p40</category><category>pascal</category><category>pytorch</category><category>quantization</category><category>rdna 3.5</category><category>rocm</category><guid>https://tinycomputers.io/posts/image-editing-on-10-year-old-gpus-nvidia-p40-vs-amd-strix-halo.html</guid><pubDate>Tue, 17 Feb 2026 18:00:00 GMT</pubDate></item><item><title>Partial LLM Loading: Running Models Too Big for VRAM</title><link>https://tinycomputers.io/posts/partial-llm-loading-running-models-too-big-for-vram.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/partial-llm-loading-running-models-too-big-for-vram.mp3" type="audio/mpeg"&gt;
Your browser does not support the audio element.
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;6:59 · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;What happens when you want to run a 70B parameter model but only have 24GB of VRAM? Traditionally, you either quantize aggressively, &lt;a href="https://baud.rs/BJLKdd"&gt;rent cloud GPUs&lt;/a&gt;, or accept that the model is simply out of reach. But there's a third option that's becoming increasingly viable: partial loading, where you keep some layers on the CPU or disk and stream them to the GPU on demand.&lt;/p&gt;
&lt;p&gt;I spent a couple days testing partial loading strategies on an &lt;a href="https://baud.rs/3vAejv"&gt;AMD Strix Halo APU with 128GB of unified memory&lt;/a&gt;, configured with 96GB allocated to VRAM, trying to answer a simple question: can you actually run models that don't fit in VRAM, and if so, how much performance do you sacrifice?&lt;/p&gt;
&lt;p&gt;The answer turns out to be: yes, you can, and the performance penalty is more nuanced than I expected.&lt;/p&gt;
&lt;h3&gt;The Memory Problem&lt;/h3&gt;
&lt;p&gt;Large language models are memory hogs. A 7B parameter model in bfloat16 needs roughly 14GB just for the weights. A 70B model needs 140GB. An 80B MoE model might need 160GB or more. Most consumer GPUs max out at 24GB, with only a handful of prosumer cards reaching 48GB.&lt;/p&gt;
&lt;p&gt;The traditional solutions each have trade-offs:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quantization&lt;/strong&gt; reduces memory requirements by storing weights in lower precision formats. INT8 cuts memory in half. INT4 cuts it to a quarter. But quantization also reduces quality, sometimes significantly for complex reasoning tasks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Model sharding&lt;/strong&gt; across multiple GPUs works if you have multiple GPUs. Most people don't.  Early on (e.g. two years ago), this is how I experimented with models, a handful of Pascal chipset NVIDIA GPUs in a former crypto mining server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cloud inference&lt;/strong&gt; works but adds latency, costs money per token, and means your data leaves your machine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Partial loading&lt;/strong&gt; offers a fourth path: keep the model weights somewhere other than VRAM (CPU RAM, disk, NVMe) and load them into the GPU only when needed. You take a latency hit on every layer that needs to be fetched, but you can run models that would otherwise be impossible.&lt;/p&gt;
&lt;h3&gt;Understanding Transformer Layer Architecture&lt;/h3&gt;
&lt;p&gt;To understand why partial loading works, you need to understand how transformers process information. A typical LLM consists of:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Embedding layer&lt;/strong&gt;: Converts input tokens to vectors. Relatively small.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decoder layers&lt;/strong&gt;: The bulk of the model. A 70B parameter model might have 80+ decoder layers, each containing attention heads and a feed-forward network (FFN).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Final layer norm and output projection&lt;/strong&gt;: Converts the final hidden states back to token probabilities. Relatively small.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The key insight is that inference is sequential through the layers. When processing a token, you go through layer 0, then layer 1, then layer 2, and so on. You never need layer 5 while you're processing layer 3. This means you can theoretically keep only one layer's weights in VRAM at a time, loading the next layer while processing the current one.&lt;/p&gt;
&lt;p&gt;In practice, keeping &lt;em&gt;all&lt;/em&gt; layers streaming from disk adds too much latency. The sweet spot is typically keeping some layers resident in VRAM (usually the first few and last few, which see the most traffic) while streaming the middle layers on demand.&lt;/p&gt;
&lt;h3&gt;The Hardware Setup&lt;/h3&gt;
&lt;p&gt;My test machine is an AMD Strix Halo APU:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Hardware Configuration:
&lt;span class="k"&gt;-&lt;/span&gt; AMD Radeon 8060S (gfx1151)
&lt;span class="k"&gt;-&lt;/span&gt; 128GB unified memory (96GB VRAM / 32GB system)
&lt;span class="k"&gt;-&lt;/span&gt; ROCm 7.0 with HSA_OVERRIDE_GFX_VERSION=11.0.0
&lt;span class="k"&gt;-&lt;/span&gt; PyTorch 2.9.1+rocm6.3
&lt;span class="k"&gt;-&lt;/span&gt; NVMe SSD: Samsung 990 Pro 2TB (PCIe 4.0, 7450 MB/s read)
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The unified memory architecture is interesting for this experiment. On a discrete GPU, moving weights from CPU RAM to VRAM requires crossing the PCIe bus, which tops out at around 64GB/s for PCIe 4.0 x16. On the Strix Halo APU, both "GPU memory" and "CPU memory" share the same physical RAM; it's just a question of which pages are mapped for GPU access.&lt;/p&gt;
&lt;p&gt;This should give partial loading an advantage on APUs, since there's no physical data movement, just page table updates. The actual numbers bear this out, as we'll see.&lt;/p&gt;
&lt;h3&gt;Three Approaches to Partial Loading&lt;/h3&gt;
&lt;p&gt;I tested three different strategies for loading models that exceed VRAM:&lt;/p&gt;
&lt;h4&gt;1. llama.cpp with Partial GPU Offloading&lt;/h4&gt;
&lt;p&gt;The simplest approach uses &lt;a href="https://baud.rs/llamacpp"&gt;llama.cpp's&lt;/a&gt; &lt;code&gt;-ngl&lt;/code&gt; (number of GPU layers) flag. This lets you specify exactly how many transformer layers go on the GPU, with the rest staying on CPU.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;./main&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;models/llama-70b-chat.gguf&lt;span class="w"&gt; &lt;/span&gt;-ngl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;35&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The capital of France is"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With a 70B model that has 80 layers, setting &lt;code&gt;-ngl 35&lt;/code&gt; puts roughly 44% of the model on the GPU and 56% on CPU. The GPU handles the compute-intensive matrix multiplications, while the CPU layers run on AMD's Zen cores.&lt;/p&gt;
&lt;p&gt;Advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simple to configure&lt;/li&gt;
&lt;li&gt;Automatic handling of which layers go where&lt;/li&gt;
&lt;li&gt;Works with GGUF quantized models&lt;/li&gt;
&lt;li&gt;CPU layers use optimized AVX-512 implementations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Disadvantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Static partitioning: layers stay where they're assigned&lt;/li&gt;
&lt;li&gt;CPU inference is much slower than GPU&lt;/li&gt;
&lt;li&gt;Limited to llama.cpp's supported architectures&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. HuggingFace Accelerate Disk Offloading&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://baud.rs/accelerate"&gt;HuggingFace's Accelerate library&lt;/a&gt; provides &lt;code&gt;device_map="auto"&lt;/code&gt; with disk offloading:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;transformers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;accelerate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;infer_auto_device_map&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"meta-llama/Llama-3.2-70B-Instruct"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;device_map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"auto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;offload_folder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"./offload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;torch_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When VRAM is insufficient, Accelerate automatically spills layers to disk (or CPU RAM if you use &lt;code&gt;offload_buffers=True&lt;/code&gt;). During inference, layers are loaded as needed.&lt;/p&gt;
&lt;p&gt;Advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Works with any HuggingFace model&lt;/li&gt;
&lt;li&gt;Automatic layer placement decisions&lt;/li&gt;
&lt;li&gt;Can use disk for infinite capacity&lt;/li&gt;
&lt;li&gt;Integrates with the broader HuggingFace ecosystem&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Disadvantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Disk I/O is slow (even NVMe)&lt;/li&gt;
&lt;li&gt;Layer loading happens synchronously&lt;/li&gt;
&lt;li&gt;Each token generation can require full model traversal&lt;/li&gt;
&lt;li&gt;Memory peaks during layer swaps&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. oLLM Layer Streaming&lt;/h4&gt;
&lt;p&gt;The most sophisticated approach I tested was &lt;a href="https://baud.rs/ollm"&gt;oLLM&lt;/a&gt;, a library designed specifically for layer-by-layer streaming from SSD to GPU. Unlike HuggingFace's approach, oLLM implements asynchronous layer prefetching: while one layer is processing on the GPU, the next layer is being loaded.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;ollm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Inference&lt;/span&gt;

&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Inference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"llama3-1B-chat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cuda:0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ini_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"./models/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Offload half the layers to CPU&lt;/span&gt;
&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;offload_layers_to_cpu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layers_num&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Generate&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"The capital of France is"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The library instruments each layer load, giving you visibility into the streaming behavior:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;layer_load&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.004&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.004&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.004&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.004&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;0.242&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This tells you each layer took about 4ms to load, and the total token generation time was 242ms.&lt;/p&gt;
&lt;p&gt;Advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Asynchronous prefetching reduces latency&lt;/li&gt;
&lt;li&gt;Per-layer timing instrumentation&lt;/li&gt;
&lt;li&gt;Designed specifically for memory-constrained scenarios&lt;/li&gt;
&lt;li&gt;Can leverage GPU Direct Storage (GDS) for faster NVMe-to-GPU transfers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Disadvantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Limited model architecture support&lt;/li&gt;
&lt;li&gt;Requires transformers 4.x (incompatible with 5.0)&lt;/li&gt;
&lt;li&gt;Less mature than llama.cpp or HuggingFace&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Benchmarking Partial Loading&lt;/h3&gt;
&lt;p&gt;I ran a series of tests with Llama 3.2 1B (16 layers, 2.8GB model size) to measure the impact of partial loading:&lt;/p&gt;
&lt;h4&gt;Test Configuration&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"llama3-1B-chat"&lt;/span&gt;
&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The capital of France is"&lt;/span&gt;
&lt;span class="n"&gt;max_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="n"&gt;configurations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"gpu_layers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cpu_layers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;# Full GPU&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"gpu_layers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cpu_layers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;# 75% GPU&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"gpu_layers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cpu_layers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;    &lt;span class="c1"&gt;# 50% GPU&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"gpu_layers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cpu_layers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;# 25% GPU&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Results: oLLM Layer Streaming&lt;/h4&gt;
&lt;p&gt;With the oLLM library and 8 of 16 layers offloaded to CPU:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GPU&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CPU&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;
&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;
&lt;span class="n"&gt;First&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;242&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="n"&gt;Per&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;average&lt;/span&gt;
&lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Correct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"The capital of France is Paris."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The layer load times are interesting. At 4ms per layer, you might expect significant overhead when 8 layers need to be fetched from CPU RAM. But because oLLM prefetches the next layer while the current one is executing, the effective latency impact is much smaller.&lt;/p&gt;
&lt;p&gt;On a discrete GPU with PCIe transfers, these numbers would be different. Loading a 200MB layer across PCIe 4.0 x16 takes about 3ms at full bandwidth. But PCIe rarely achieves full bandwidth due to protocol overhead, so real-world numbers are typically 4-6ms per layer, similar to what I measured on the APU.&lt;/p&gt;
&lt;h4&gt;The Quality Question&lt;/h4&gt;
&lt;p&gt;A critical question with partial loading: does offloading layers affect output quality?&lt;/p&gt;
&lt;p&gt;The answer is no, with an important caveat. Partial loading doesn't change the weights; it just changes where they're stored. The same matrices participate in the same computations. The outputs are bit-identical to full GPU inference.&lt;/p&gt;
&lt;p&gt;The caveat is that some partial loading implementations use reduced precision for CPU layers (FP32 instead of bfloat16, or even FP16) to speed up CPU computation. This can introduce small numerical differences. In my testing with oLLM, both GPU and CPU layers used the same bfloat16 precision, so outputs matched exactly.&lt;/p&gt;
&lt;h3&gt;Practical Performance Analysis&lt;/h3&gt;
&lt;p&gt;Let's break down what partial loading actually costs in terms of latency.&lt;/p&gt;
&lt;h4&gt;Layer Loading Overhead&lt;/h4&gt;
&lt;p&gt;For a model with N layers, where K layers are on CPU:
- Each token generation requires K layer loads
- If each load takes T_load milliseconds
- The total added latency per token is approximately K * T_load&lt;/p&gt;
&lt;p&gt;With oLLM's prefetching, the effective latency is lower because loads overlap with computation. In my tests:
- K = 8 layers on CPU
- T_load = 4ms per layer
- Naive overhead = 32ms per token
- Actual overhead (with prefetching) = ~10-15ms per token&lt;/p&gt;
&lt;h4&gt;Memory Bandwidth Bottleneck&lt;/h4&gt;
&lt;p&gt;The real constraint isn't CPU speed; it's memory bandwidth. A single transformer layer in a 70B model might be 800MB-1.2GB. Loading this from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NVMe SSD: 7.4GB/s = 108-162ms per layer&lt;/li&gt;
&lt;li&gt;DDR5 RAM: 80GB/s = 10-15ms per layer&lt;/li&gt;
&lt;li&gt;PCIe 4.0 x16: 32GB/s = 25-37ms per layer (in practice)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is why oLLM's authors recommend fast NVMe SSDs (&lt;a href="https://baud.rs/pqpsCq"&gt;Samsung 990 Pro&lt;/a&gt;, &lt;a href="https://baud.rs/XluQ37"&gt;WD SN850X&lt;/a&gt;) and ideally GPU Direct Storage, which bypasses the CPU entirely for disk-to-GPU transfers.&lt;/p&gt;
&lt;h4&gt;Token Generation Speed Comparison&lt;/h4&gt;
&lt;p&gt;For the Llama 3.2 1B model (16 layers total), I ran benchmarks across multiple prompts and averaging the results:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Avg Tokens/sec&lt;/th&gt;
&lt;th&gt;Avg Inference Time&lt;/th&gt;
&lt;th&gt;Load Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full GPU (0 offloaded)&lt;/td&gt;
&lt;td&gt;1.92 tok/s&lt;/td&gt;
&lt;td&gt;13.90s&lt;/td&gt;
&lt;td&gt;0.46s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 layers on CPU&lt;/td&gt;
&lt;td&gt;2.23 tok/s&lt;/td&gt;
&lt;td&gt;11.09s&lt;/td&gt;
&lt;td&gt;0.55s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 layers on CPU&lt;/td&gt;
&lt;td&gt;2.26 tok/s&lt;/td&gt;
&lt;td&gt;10.87s&lt;/td&gt;
&lt;td&gt;0.65s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12 layers on CPU&lt;/td&gt;
&lt;td&gt;3.36 tok/s&lt;/td&gt;
&lt;td&gt;7.30s&lt;/td&gt;
&lt;td&gt;0.75s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Wait, that's backwards from what you'd expect. More layers on CPU resulted in &lt;em&gt;faster&lt;/em&gt; inference?&lt;/p&gt;
&lt;p&gt;This counterintuitive result makes sense when you consider the Strix Halo's unified memory architecture. Unlike a discrete GPU where CPU-to-GPU transfers cross the PCIe bus, the APU's "CPU memory" and "GPU memory" are the same physical RAM. Moving layers between them is essentially just a page table operation, not a data copy.&lt;/p&gt;
&lt;p&gt;The performance improvement with more offloading likely comes from reduced memory bandwidth contention. When all layers are "on GPU," they're competing for the same memory channels. With layer streaming, only the active layer's weights occupy high-bandwidth GPU memory paths, while inactive layers sit in lower-priority memory regions.&lt;/p&gt;
&lt;p&gt;This finding suggests that on unified memory systems (AMD APUs, Apple Silicon), partial loading might actually be &lt;em&gt;preferable&lt;/em&gt; to full GPU loading for memory-bandwidth-bound workloads. The conventional wisdom (that GPU is always faster) doesn't hold when there's no physical separation between GPU and CPU memory.&lt;/p&gt;
&lt;h3&gt;Transformer Version Compatibility Issues&lt;/h3&gt;
&lt;p&gt;One challenge I encountered was library compatibility. oLLM was designed for transformers 4.x, and when I initially ran it with &lt;a href="https://baud.rs/transformers"&gt;transformers 5.0&lt;/a&gt;, I hit several errors:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Qwen3NextExperts'&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;iterable&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This error occurred because transformers 5.0 changed how Mixture of Experts (MoE) layers expose their expert modules. The oLLM library's layer streaming code assumed it could iterate over &lt;code&gt;self.mlp.experts&lt;/code&gt;, but the new implementation uses a different structure.&lt;/p&gt;
&lt;p&gt;There were also weight shape mismatches:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;model.layers.0.input_layernorm.weight: found shape torch.Size([2048])
in the checkpoint and torch.Size([0]) in the model instantiated
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This happened because oLLM creates placeholder layers with zero-size tensors to save memory, then loads the actual weights on demand. The new transformers version changed how these placeholder shapes were inferred.&lt;/p&gt;
&lt;p&gt;The solution was straightforward: pin transformers to version 4.57.6:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'transformers&amp;lt;5.0'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is a common pattern with cutting-edge ML libraries. The ecosystem moves fast, and specialized tools often lag behind major version updates.&lt;/p&gt;
&lt;h3&gt;Storage and System Requirements&lt;/h3&gt;
&lt;p&gt;Before diving into partial loading, it's worth understanding the storage requirements. Unlike full GPU loading where you only need enough VRAM, partial loading requires sufficient storage capacity and bandwidth.&lt;/p&gt;
&lt;h4&gt;Disk Space Calculations&lt;/h4&gt;
&lt;p&gt;Model files on disk are typically stored in safetensors or GGUF format. A rough calculation:
- 7B model (bfloat16): ~14GB
- 13B model (bfloat16): ~26GB
- 70B model (bfloat16): ~140GB
- 70B model (GGUF Q4_K_M): ~40GB&lt;/p&gt;
&lt;p&gt;For oLLM's layer streaming, you also need the model to be split into per-layer shards, which the library handles automatically during the first load. This adds temporary storage overhead during the conversion process.&lt;/p&gt;
&lt;h4&gt;RAM Requirements&lt;/h4&gt;
&lt;p&gt;CPU offloading means the offloaded layers live in system RAM. If you're offloading 40 of 80 layers from a 70B model, you need roughly 70GB of system RAM available, in addition to whatever the operating system and other applications need.&lt;/p&gt;
&lt;p&gt;On my Strix Halo system with 128GB unified memory (96GB allocated to VRAM, 32GB to system), this gets interesting. The "CPU" portion of memory and the "GPU" portion share the same physical DIMMs. Allocating layers to "CPU" really just means they're in a different memory region that the GPU can still access, but through a different (slower) path.&lt;/p&gt;
&lt;h4&gt;SSD Endurance Considerations&lt;/h4&gt;
&lt;p&gt;If you're streaming weights from disk rather than CPU RAM, consider your SSD's endurance. A 70B model with 80 layers means moving roughly 1.75GB per token generated (all layers traversed once). Generate 1000 tokens and you've read 1.75TB from the SSD.&lt;/p&gt;
&lt;p&gt;For occasional use, this is fine. For continuous operation (like a chatbot running 24/7), you might wear out a consumer SSD within months. Enterprise SSDs with higher TBW (Terabytes Written) ratings are worth considering for heavy use cases, or preferring CPU RAM offloading over disk offloading.&lt;/p&gt;
&lt;h4&gt;Memory Mapping and Page Tables&lt;/h4&gt;
&lt;p&gt;Under the hood, partial loading relies on the operating system's memory management. When a layer is "loaded" to the GPU, this typically involves:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reading the layer from storage (if not already in RAM)&lt;/li&gt;
&lt;li&gt;Pinning the memory pages so they can't be swapped&lt;/li&gt;
&lt;li&gt;Mapping the pages into GPU-accessible memory space&lt;/li&gt;
&lt;li&gt;Synchronizing to ensure the GPU sees the updated data&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;On Linux, this uses &lt;code&gt;mmap()&lt;/code&gt; and &lt;code&gt;mlock()&lt;/code&gt; syscalls. The &lt;code&gt;vm.max_map_count&lt;/code&gt; sysctl may need to be increased for very large models:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Check current value&lt;/span&gt;
cat&lt;span class="w"&gt; &lt;/span&gt;/proc/sys/vm/max_map_count

&lt;span class="c1"&gt;# Increase if needed&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;sysctl&lt;span class="w"&gt; &lt;/span&gt;-w&lt;span class="w"&gt; &lt;/span&gt;vm.max_map_count&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1048576&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I hit this limit when testing 70B+ models and saw cryptic "cannot allocate memory" errors until increasing the map count.&lt;/p&gt;
&lt;h3&gt;When Partial Loading Makes Sense&lt;/h3&gt;
&lt;p&gt;Based on my testing, here's when partial loading is a good fit:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good Use Cases:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Batch processing where latency isn't critical (overnight analysis, embedding generation)&lt;/li&gt;
&lt;li&gt;Interactive use with smaller models where the overhead is manageable&lt;/li&gt;
&lt;li&gt;Running larger models occasionally without investing in more VRAM&lt;/li&gt;
&lt;li&gt;Testing different model sizes before committing to hardware&lt;/li&gt;
&lt;li&gt;APU systems where CPU-GPU transfer costs are minimal&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Poor Use Cases:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real-time applications (chatbots, live transcription)&lt;/li&gt;
&lt;li&gt;High-throughput production systems&lt;/li&gt;
&lt;li&gt;When quantization gives acceptable quality with lower overhead&lt;/li&gt;
&lt;li&gt;Systems with slow storage (spinning disks, older SSDs)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The break-even point depends heavily on your specific hardware. On my APU system with unified memory, offloading 50% of layers costs about 33% of throughput. On a discrete GPU with PCIe 3.0, the same configuration might cost 60-70%.&lt;/p&gt;
&lt;h3&gt;Future Directions&lt;/h3&gt;
&lt;p&gt;Several developments could make partial loading more practical:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GPU Direct Storage (GDS):&lt;/strong&gt; NVIDIA's GDS and AMD's equivalent allow direct SSD-to-GPU transfers, bypassing the CPU and PCIe. Early implementations show 3-4x improvements in layer load times.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Better Prefetching Algorithms:&lt;/strong&gt; Current implementations use simple next-layer prefetching. More sophisticated approaches could predict multiple layers ahead, or prioritize layers that are accessed most frequently (relevant for some architectures with skip connections).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hardware Evolution:&lt;/strong&gt; Unified memory architectures like Apple Silicon and AMD APUs eliminate the CPU-GPU transfer bottleneck entirely. As these architectures gain more memory capacity, partial loading becomes increasingly attractive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compression:&lt;/strong&gt; Applying neural compression to stored weights (not quantization, but actual neural codecs) could reduce the bandwidth requirements by 2-4x without quality loss.&lt;/p&gt;
&lt;h3&gt;Building a Benchmark Framework&lt;/h3&gt;
&lt;p&gt;For those who want to measure partial loading on their own hardware, here's the framework I developed:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dataclasses&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;BenchmarkResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;total_layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;gpu_layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;cpu_layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;loading_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;  &lt;span class="c1"&gt;# 'partial' or 'full'&lt;/span&gt;
    &lt;span class="n"&gt;load_time_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;tokens_generated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;total_inference_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;tokens_per_second&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;per_layer_load_times&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;peak_vram_gb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;benchmark_ollm_inference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;offload_layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BenchmarkResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;ollm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Inference&lt;/span&gt;

    &lt;span class="c1"&gt;# Measure loading time&lt;/span&gt;
    &lt;span class="n"&gt;load_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Inference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cuda:0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ini_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"./models/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;force_download&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;total_layers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;offload_layers&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;offload_layers_to_cpu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layers_num&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;offload_layers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;load_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;load_start&lt;/span&gt;

    &lt;span class="c1"&gt;# Measure inference time&lt;/span&gt;
    &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;infer_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;infer_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;infer_start&lt;/span&gt;

    &lt;span class="c1"&gt;# Count tokens&lt;/span&gt;
    &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;BenchmarkResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;total_layers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;total_layers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;gpu_layers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;total_layers&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;offload_layers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cpu_layers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;offload_layers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;loading_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'partial'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;offload_layers&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s1"&gt;'full'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;load_time_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;load_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tokens_generated&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;total_inference_time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;infer_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tokens_per_second&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;infer_time&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;infer_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;peak_vram_gb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_memory_allocated&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1e9&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This framework measures the key metrics: loading time, inference time, tokens per second, and VRAM usage. Run it with different &lt;code&gt;offload_layers&lt;/code&gt; values to map out the performance curve for your specific hardware.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Partial LLM loading isn't a silver bullet, but it's a valuable technique for expanding what's possible on memory-constrained hardware. On my 128GB APU system, I found something unexpected: partial loading with 12 of 16 layers on CPU actually &lt;em&gt;outperformed&lt;/em&gt; full GPU loading by 75% (3.36 tok/s vs 1.92 tok/s).&lt;/p&gt;
&lt;p&gt;The key takeaways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Unified memory changes everything.&lt;/strong&gt; On APUs and Apple Silicon, the conventional wisdom that "GPU is always faster" doesn't hold. Reduced memory bandwidth contention can make partial loading preferable even when you have enough VRAM.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Prefetching is essential.&lt;/strong&gt; Naive layer loading is too slow. Libraries like oLLM that prefetch the next layer during current layer computation can reduce overhead by 50% or more.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Memory bandwidth matters more than CPU speed.&lt;/strong&gt; The bottleneck is getting bytes from storage/RAM to the GPU, not processing them once they're there.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Library maturity varies.&lt;/strong&gt; Expect compatibility issues with newer transformers versions. Pin your dependencies; oLLM requires transformers 4.x.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Quality is preserved.&lt;/strong&gt; Partial loading changes where weights live, not what they are. Outputs match full GPU inference exactly (assuming matching precision).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Benchmark your specific hardware.&lt;/strong&gt; My results on a Strix Halo APU won't match discrete GPU performance. The only way to know what works best is to measure it.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For batch processing and experimentation, partial loading lets you access models that would otherwise require more expensive hardware. For unified memory systems specifically, partial loading might be the &lt;em&gt;optimal&lt;/em&gt; configuration, not just a fallback.&lt;/p&gt;
&lt;p&gt;The era of "if it doesn't fit in VRAM, you can't run it" is ending. With the right techniques, nearly any model becomes accessible, and on the right hardware, you might even get a performance bonus for your trouble.&lt;/p&gt;</description><category>amd</category><category>gpu memory</category><category>layer streaming</category><category>llm</category><category>machine learning</category><category>memory optimization</category><category>ollm</category><category>partial loading</category><category>pytorch</category><category>rocm</category><category>strix halo</category><category>transformers</category><category>vram</category><guid>https://tinycomputers.io/posts/partial-llm-loading-running-models-too-big-for-vram.html</guid><pubDate>Thu, 05 Feb 2026 16:00:00 GMT</pubDate></item><item><title>Running vLLM in Docker with AMD ROCm and the Continue.dev CLI</title><link>https://tinycomputers.io/posts/running-vllm-in-docker-with-amd-rocm-and-the-continuedev-cli.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/running-vllm-in-docker-with-amd-rocm-and-the-continuedev-cli_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;14 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;If you've been following the AI coding assistant space, you've probably noticed that most tools assume you're running NVIDIA hardware or using a cloud API. But what if you have AMD hardware and want to run large language models locally with full tool-calling support? This guide walks through setting up vLLM with AMD ROCm in Docker and connecting it to Continue.dev's &lt;code&gt;cn&lt;/code&gt; command-line coding assistant.&lt;/p&gt;
&lt;h3&gt;Why vLLM?&lt;/h3&gt;
&lt;p&gt;There are several options for running LLMs locally: llama.cpp, Ollama, and vLLM being the most popular. I chose vLLM for a specific reason: &lt;strong&gt;tool calling support&lt;/strong&gt;. vLLM implements the OpenAI-compatible API with proper function calling, which means coding assistants can use tools like file reading, code execution, and search. This is critical for getting a capable coding assistant rather than just a chat interface.&lt;/p&gt;
&lt;p&gt;vLLM also offers excellent performance through continuous batching, PagedAttention for efficient memory management, and support for a wide range of models. The trade-off is that it's more resource-intensive than llama.cpp, but if you have the VRAM, the capabilities are worth it.&lt;/p&gt;
&lt;h3&gt;Hardware Setup&lt;/h3&gt;
&lt;p&gt;For this guide, I'm using an AMD Strix Halo system (Ryzen AI MAX+ 395) with 128GB of unified memory. If you're looking for a similar setup, the &lt;a href="https://baud.rs/gmVPEI"&gt;GMKtec EVO-X2&lt;/a&gt; is one of the first mini PCs available with this chip. The integrated GPU shows up as &lt;code&gt;gfx1151&lt;/code&gt; in ROCm. However, this guide should work for any AMD GPU supported by ROCm, including discrete cards like the &lt;a href="https://baud.rs/0uWeZN"&gt;RX 7900 XTX&lt;/a&gt;, MI100, or MI250.&lt;/p&gt;
&lt;p&gt;The unified memory architecture on Strix Halo is particularly interesting for LLM inference. Unlike discrete GPUs where you're limited by VRAM, the CPU and GPU share the same memory pool. This means you can run models that would normally require multiple high-end GPUs on a single chip, as long as you have enough system RAM.&lt;/p&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;Before starting, you'll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An AMD GPU supported by ROCm&lt;/li&gt;
&lt;li&gt;Docker installed on your system&lt;/li&gt;
&lt;li&gt;ROCm drivers installed (version 6.0 or later recommended)&lt;/li&gt;
&lt;li&gt;At least 16GB of RAM (more for larger models)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To verify your ROCm installation, run:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;rocminfo&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;gfx
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You should see your GPU architecture listed (e.g., &lt;code&gt;gfx1100&lt;/code&gt; for RDNA3, &lt;code&gt;gfx1151&lt;/code&gt; for Strix Halo).&lt;/p&gt;
&lt;h3&gt;Running vLLM in Docker&lt;/h3&gt;
&lt;p&gt;The easiest way to get vLLM running with ROCm is through Docker. The ROCm team maintains nightly images that include all necessary dependencies.&lt;/p&gt;
&lt;h4&gt;Pulling the Image&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;docker&lt;span class="w"&gt; &lt;/span&gt;pull&lt;span class="w"&gt; &lt;/span&gt;rocm/vllm-dev:nightly
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This image is large (several GB) as it includes the full ROCm stack, PyTorch, and vLLM with all dependencies.&lt;/p&gt;
&lt;h4&gt;Starting the Container&lt;/h4&gt;
&lt;p&gt;Start the container with GPU access and port forwarding:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;vllm-dev&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--device&lt;span class="o"&gt;=&lt;/span&gt;/dev/kfd&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--device&lt;span class="o"&gt;=&lt;/span&gt;/dev/dri&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--group-add&lt;span class="w"&gt; &lt;/span&gt;video&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--cap-add&lt;span class="o"&gt;=&lt;/span&gt;SYS_PTRACE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--security-opt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;seccomp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unconfined&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8000&lt;/span&gt;:8000&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;~/.cache/huggingface:/root/.cache/huggingface&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;rocm/vllm-dev:nightly&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;tail&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;/dev/null
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let me break down the important flags:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--device=/dev/kfd&lt;/code&gt; and &lt;code&gt;--device=/dev/dri&lt;/code&gt;: Give the container access to the GPU&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--group-add video&lt;/code&gt;: Required for GPU access permissions&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p 8000:8000&lt;/code&gt;: Expose the vLLM API port&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v ~/.cache/huggingface:/root/.cache/huggingface&lt;/code&gt;: Persist downloaded models between container restarts&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tail -f /dev/null&lt;/code&gt;: Keep the container running so we can exec into it&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Installing AMD SMI (Important!)&lt;/h4&gt;
&lt;p&gt;Before starting vLLM, you need to install the AMD SMI Python package inside the container. This is required for vLLM to detect the ROCm platform correctly:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-it&lt;span class="w"&gt; &lt;/span&gt;vllm-dev&lt;span class="w"&gt; &lt;/span&gt;bash
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;/opt/rocm/share/amd_smi
&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Without this step, vLLM will fail with an "UnspecifiedPlatform" error because it can't detect your AMD GPU.&lt;/p&gt;
&lt;h4&gt;Starting vLLM&lt;/h4&gt;
&lt;p&gt;Now start the vLLM server inside the container:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;vllm-dev&lt;span class="w"&gt; &lt;/span&gt;bash&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'vllm serve Qwen/Qwen2.5-7B-Instruct \&lt;/span&gt;
&lt;span class="s1"&gt;  --max-model-len 32768 \&lt;/span&gt;
&lt;span class="s1"&gt;  --enable-auto-tool-choice \&lt;/span&gt;
&lt;span class="s1"&gt;  --tool-call-parser hermes \&lt;/span&gt;
&lt;span class="s1"&gt;  --host 0.0.0.0 \&lt;/span&gt;
&lt;span class="s1"&gt;  --port 8000 &amp;gt; /tmp/vllm.log 2&amp;gt;&amp;amp;1'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The key flags here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Qwen/Qwen2.5-7B-Instruct&lt;/code&gt;: The model to serve. Qwen 2.5 is excellent for coding tasks and supports tool calling.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--max-model-len 32768&lt;/code&gt;: Maximum context length. Coding assistants need long contexts for system prompts and code.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--enable-auto-tool-choice&lt;/code&gt;: Enable function calling support&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--tool-call-parser hermes&lt;/code&gt;: Use the Hermes format for tool calls, which Qwen supports&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first startup takes a while as vLLM downloads the model weights, compiles CUDA graphs, and warms up. Monitor progress with:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vllm-dev&lt;span class="w"&gt; &lt;/span&gt;tail&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;/tmp/vllm.log
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You'll see it loading model shards, then capturing CUDA graphs. Once you see "Uvicorn running on http://0.0.0.0:8000", the server is ready.&lt;/p&gt;
&lt;h4&gt;Verifying the Server&lt;/h4&gt;
&lt;p&gt;Test that the server is responding:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;curl&lt;span class="w"&gt; &lt;/span&gt;http://localhost:8000/v1/models
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You should see JSON output showing the loaded model with its maximum context length.&lt;/p&gt;
&lt;p&gt;For a quick inference test:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;curl&lt;span class="w"&gt; &lt;/span&gt;http://localhost:8000/v1/chat/completions&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-H&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{&lt;/span&gt;
&lt;span class="s1"&gt;    "model": "Qwen/Qwen2.5-7B-Instruct",&lt;/span&gt;
&lt;span class="s1"&gt;    "messages": [{"role": "user", "content": "Hello!"}],&lt;/span&gt;
&lt;span class="s1"&gt;    "max_tokens": 50&lt;/span&gt;
&lt;span class="s1"&gt;  }'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Choosing a Model&lt;/h3&gt;
&lt;p&gt;The model you choose depends on your available memory and performance requirements. Here's a rough guide:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;VRAM Required&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen2.5-0.5B-Instruct&lt;/td&gt;
&lt;td&gt;~2GB&lt;/td&gt;
&lt;td&gt;Testing, very fast responses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen2.5-7B-Instruct&lt;/td&gt;
&lt;td&gt;~16GB&lt;/td&gt;
&lt;td&gt;Good balance of speed and capability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen2.5-32B-Instruct&lt;/td&gt;
&lt;td&gt;~70GB&lt;/td&gt;
&lt;td&gt;Best quality, slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-Coder-33B&lt;/td&gt;
&lt;td&gt;~70GB&lt;/td&gt;
&lt;td&gt;Specialized for code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For my system with 96GB allocated as VRAM, the 7B model leaves about 65GB free for KV cache, allowing concurrent requests with long contexts. The 32B model fits but leaves less headroom.&lt;/p&gt;
&lt;h3&gt;Setting Up Continue.dev CLI&lt;/h3&gt;
&lt;p&gt;Continue.dev is primarily known as a VS Code extension, but they also offer a command-line interface called &lt;code&gt;cn&lt;/code&gt; that provides an AI coding assistant directly in your terminal.&lt;/p&gt;
&lt;h4&gt;Installing cn&lt;/h4&gt;
&lt;p&gt;The cn CLI is available via npm:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;npm&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;@anthropic/cn
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Or if you prefer not to install globally, you can use npx:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;npx&lt;span class="w"&gt; &lt;/span&gt;@anthropic/cn
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Configuring cn for vLLM&lt;/h4&gt;
&lt;p&gt;Create or edit &lt;code&gt;~/.continue/config.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Local Assistant&lt;/span&gt;
&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1.0.0&lt;/span&gt;
&lt;span class="nt"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;v1&lt;/span&gt;

&lt;span class="nt"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Qwen2.5-7B-vLLM&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;openai&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Qwen/Qwen2.5-7B-Instruct&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;apiBase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;http://localhost:8000/v1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;none&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;chat&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;edit&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;60000000&lt;/span&gt;

&lt;span class="nt"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;code&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;docs&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;diff&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;terminal&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;problems&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;folder&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;codebase&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The important settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;provider: openai&lt;/code&gt;: Use the OpenAI-compatible API format&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apiBase&lt;/code&gt;: Point to your vLLM server&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apiKey: none&lt;/code&gt;: vLLM doesn't require authentication by default&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timeout&lt;/code&gt;: Set high for longer operations&lt;/li&gt;
&lt;li&gt;&lt;code&gt;context&lt;/code&gt;: Enable various context providers for code understanding&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Using cn&lt;/h4&gt;
&lt;p&gt;Run cn from your project directory:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;cn&lt;span class="w"&gt; &lt;/span&gt;--config&lt;span class="w"&gt; &lt;/span&gt;~/.continue/config.yaml
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This starts an interactive session where you can ask questions about your codebase, request changes, and have the assistant use tools to explore and modify files.&lt;/p&gt;
&lt;p&gt;For quick one-off queries, use the print mode:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What does this project do?"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cn&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;--config&lt;span class="w"&gt; &lt;/span&gt;~/.continue/config.yaml
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;-p&lt;/code&gt; flag prints the response and exits, useful for scripting or quick questions.&lt;/p&gt;
&lt;h4&gt;Example Session&lt;/h4&gt;
&lt;p&gt;Here's what a typical session looks like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;cn&lt;span class="w"&gt; &lt;/span&gt;--config&lt;span class="w"&gt; &lt;/span&gt;~/.continue/config.yaml
&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;What&lt;span class="w"&gt; &lt;/span&gt;files&lt;span class="w"&gt; &lt;/span&gt;handle&lt;span class="w"&gt; &lt;/span&gt;authentication&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;this&lt;span class="w"&gt; &lt;/span&gt;project?

I&lt;span class="s1"&gt;'ll search the codebase for authentication-related code.&lt;/span&gt;

&lt;span class="s1"&gt;[Uses grep tool to search for "auth", "login", "session"]&lt;/span&gt;

&lt;span class="s1"&gt;Based on my search, authentication is handled in:&lt;/span&gt;
&lt;span class="s1"&gt;- src/middleware/auth.js - JWT verification middleware&lt;/span&gt;
&lt;span class="s1"&gt;- src/routes/login.js - Login endpoint&lt;/span&gt;
&lt;span class="s1"&gt;- src/models/user.js - User model with password hashing&lt;/span&gt;

&lt;span class="s1"&gt;&amp;gt; Add rate limiting to the login endpoint&lt;/span&gt;

&lt;span class="s1"&gt;I'&lt;/span&gt;ll&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;current&lt;span class="w"&gt; &lt;/span&gt;login&lt;span class="w"&gt; &lt;/span&gt;route&lt;span class="w"&gt; &lt;/span&gt;and&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;rate&lt;span class="w"&gt; &lt;/span&gt;limiting...
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The assistant can read files, search code, make edits, and run commands, all while maintaining context about your project.&lt;/p&gt;
&lt;h3&gt;Troubleshooting&lt;/h3&gt;
&lt;h4&gt;"Context length exceeded" Error&lt;/h4&gt;
&lt;p&gt;If cn fails with a context length error, your vLLM server's &lt;code&gt;--max-model-len&lt;/code&gt; is too low. The Continue CLI adds substantial system prompts. Restart vLLM with at least 32768 tokens:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vllm-dev&lt;span class="w"&gt; &lt;/span&gt;pkill&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vllm serve"&lt;/span&gt;
docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;vllm-dev&lt;span class="w"&gt; &lt;/span&gt;bash&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'vllm serve Qwen/Qwen2.5-7B-Instruct \&lt;/span&gt;
&lt;span class="s1"&gt;  --max-model-len 32768 \&lt;/span&gt;
&lt;span class="s1"&gt;  --enable-auto-tool-choice \&lt;/span&gt;
&lt;span class="s1"&gt;  --tool-call-parser hermes \&lt;/span&gt;
&lt;span class="s1"&gt;  --host 0.0.0.0 &amp;gt; /tmp/vllm.log 2&amp;gt;&amp;amp;1'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;GPU Memory Not Released&lt;/h4&gt;
&lt;p&gt;If vLLM fails to start due to insufficient memory, the previous instance may not have released GPU memory. The cleanest fix is to restart the container:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;docker&lt;span class="w"&gt; &lt;/span&gt;restart&lt;span class="w"&gt; &lt;/span&gt;vllm-dev
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then start vLLM again.&lt;/p&gt;
&lt;h4&gt;Slow Inference&lt;/h4&gt;
&lt;p&gt;If inference is slow, check that GPU acceleration is actually being used:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;watch&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rocm-smi
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You should see GPU utilization when generating tokens. If utilization is 0%, there may be a driver or permission issue.&lt;/p&gt;
&lt;h3&gt;Performance Notes&lt;/h3&gt;
&lt;p&gt;On Strix Halo with the 7B model, I see around 30-50 tokens per second for generation. The first request after starting is slower due to KV cache warmup. With the 32B model, speed drops to 10-15 tokens per second but quality improves significantly.&lt;/p&gt;
&lt;p&gt;The unified memory architecture means there's no PCIe bottleneck for loading model weights, which helps with the initial prompt processing. However, the iGPU compute is slower than a discrete high-end GPU, so this setup prioritizes accessibility over raw speed.&lt;/p&gt;
&lt;h3&gt;Remote Access&lt;/h3&gt;
&lt;p&gt;If your vLLM server is running on a different machine (like a dedicated inference server), you'll need to update the &lt;code&gt;apiBase&lt;/code&gt; in your config to point to that machine's IP address:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;apiBase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;http://192.168.1.100:8000/v1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Make sure port 8000 is accessible through any firewalls. For secure remote access over the internet, consider setting up a VPN or SSH tunnel rather than exposing the port directly:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;-L&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8000&lt;/span&gt;:localhost:8000&lt;span class="w"&gt; &lt;/span&gt;user@remote-server
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This forwards local port 8000 to the remote server, so you can keep using &lt;code&gt;localhost:8000&lt;/code&gt; in your config while the actual inference happens remotely.&lt;/p&gt;
&lt;h3&gt;Alternative Clients&lt;/h3&gt;
&lt;p&gt;While this guide focuses on Continue.dev's &lt;code&gt;cn&lt;/code&gt; CLI, the vLLM server works with any OpenAI-compatible client. Here are a few alternatives worth considering:&lt;/p&gt;
&lt;h4&gt;Aider&lt;/h4&gt;
&lt;p&gt;Aider is another excellent terminal-based coding assistant. Install it with pip:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;aider-chat
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then connect to your vLLM server:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;aider&lt;span class="w"&gt; &lt;/span&gt;--model&lt;span class="w"&gt; &lt;/span&gt;openai/Qwen/Qwen2.5-7B-Instruct&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--openai-api-base&lt;span class="w"&gt; &lt;/span&gt;http://localhost:8000/v1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--openai-api-key&lt;span class="w"&gt; &lt;/span&gt;none
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Aider has a different interaction style than Continue, using git-aware editing and a focus on making commits. It's worth trying both to see which fits your workflow.&lt;/p&gt;
&lt;h4&gt;Open WebUI&lt;/h4&gt;
&lt;p&gt;For a graphical interface, Open WebUI provides a ChatGPT-like experience that connects to local LLM servers. It's particularly nice for non-coding conversations or when you want to share access with others who prefer a web interface.&lt;/p&gt;
&lt;h4&gt;Direct API Calls&lt;/h4&gt;
&lt;p&gt;For scripting and automation, you can call the vLLM API directly. Here's a Python example:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"http://localhost:8000/v1/chat/completions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Qwen/Qwen2.5-7B-Instruct"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Explain this code: def fib(n): ..."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s2"&gt;"max_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="s2"&gt;"choices"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is useful for building custom tools or integrating LLM capabilities into existing scripts.&lt;/p&gt;
&lt;h3&gt;Keeping the Server Running&lt;/h3&gt;
&lt;p&gt;For a production-like setup where you want vLLM to start automatically and stay running, consider creating a startup script or using Docker Compose. Here's a simple approach using a shell script:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c1"&gt;# start-vllm.sh&lt;/span&gt;

docker&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;vllm-dev&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Container already running"&lt;/span&gt;

docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vllm-dev&lt;span class="w"&gt; &lt;/span&gt;pkill&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vllm serve"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null

sleep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;

docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;vllm-dev&lt;span class="w"&gt; &lt;/span&gt;bash&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'vllm serve Qwen/Qwen2.5-7B-Instruct \&lt;/span&gt;
&lt;span class="s1"&gt;  --max-model-len 32768 \&lt;/span&gt;
&lt;span class="s1"&gt;  --enable-auto-tool-choice \&lt;/span&gt;
&lt;span class="s1"&gt;  --tool-call-parser hermes \&lt;/span&gt;
&lt;span class="s1"&gt;  --host 0.0.0.0 \&lt;/span&gt;
&lt;span class="s1"&gt;  --port 8000 &amp;gt; /tmp/vllm.log 2&amp;gt;&amp;amp;1'&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vLLM starting... check logs with: docker exec vllm-dev tail -f /tmp/vllm.log"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Make it executable with &lt;code&gt;chmod +x start-vllm.sh&lt;/code&gt; and run it whenever you need to start or restart the server.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Running vLLM with ROCm opens up local AI coding assistants to AMD GPU users. Combined with Continue.dev's cn CLI, you get a capable terminal-based assistant that can understand your codebase, make edits, and use tools, all running on your own hardware with no cloud dependencies.&lt;/p&gt;
&lt;p&gt;The setup isn't as plug-and-play as using a cloud API, but the privacy benefits and lack of per-token costs make it worthwhile for regular use. And as AMD's ROCm ecosystem continues to mature, expect the experience to get smoother with each release.&lt;/p&gt;
&lt;p&gt;What I appreciate most about this setup is the flexibility. You're not locked into any particular client or workflow. The same vLLM server can power your terminal coding assistant, a web chat interface, custom scripts, and IDE integrations all at once. That's the advantage of running your own inference server: you control the stack from model selection to client interface.&lt;/p&gt;
&lt;p&gt;If you're interested in exploring further, consider trying different models (DeepSeek Coder is excellent for code-focused tasks), experimenting with quantized models for better performance, or setting up the full Continue VS Code extension alongside the CLI for a complete local AI development environment.&lt;/p&gt;</description><category>ai</category><category>amd</category><category>coding-assistant</category><category>continue</category><category>docker</category><category>llm</category><category>rocm</category><category>vllm</category><guid>https://tinycomputers.io/posts/running-vllm-in-docker-with-amd-rocm-and-the-continuedev-cli.html</guid><pubDate>Sun, 25 Jan 2026 20:13:00 GMT</pubDate></item><item><title>Running Qwen TTS on AMD Strix Halo: A Complete Guide to Local Text-to-Speech</title><link>https://tinycomputers.io/posts/qwen-tts-on-amd-strix-halo.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;p&gt;The rise of high-quality text-to-speech models has opened new possibilities for content creators, accessibility advocates, and developers alike. Qwen3-TTS, developed by Alibaba's Qwen team, represents a significant leap forward in neural TTS technology, offering natural-sounding speech synthesis with multiple speaker voices. In this guide, we'll walk through setting up Qwen3-TTS on AMD's Strix Halo platform (specifically the AI Max+ 395 with its integrated Radeon 8060S graphics) and demonstrate how we use it to generate audio narrations for blog posts right here on TinyComputers.&lt;/p&gt;
&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/qwen-tts-on-amd-strix-halo_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;16 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;Why Qwen3-TTS?&lt;/h3&gt;
&lt;p&gt;The text-to-speech landscape has evolved dramatically over the past few years. While cloud-based services like Amazon Polly, Google Cloud TTS, and ElevenLabs offer impressive quality, they come with ongoing costs, privacy considerations, and internet dependency. Local TTS solutions have historically lagged behind in quality, often producing robotic or unnatural speech.&lt;/p&gt;
&lt;p&gt;Qwen3-TTS changes this equation. The model produces remarkably natural speech with proper intonation, pacing, and emphasis. It supports multiple pre-trained speaker voices (including options like Eric, Aiden, Dylan, Serena, and others) each with distinct characteristics suitable for different content types. For technical content like our blog posts, the Eric voice provides clear, professional narration that listeners find easy to follow.&lt;/p&gt;
&lt;p&gt;The model we're using, &lt;code&gt;Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice&lt;/code&gt;, weighs in at 1.7 billion parameters. While not small, this is manageable on modern hardware and runs efficiently on GPU. The 12Hz designation refers to the audio frame rate used during generation, balancing quality with computational requirements.&lt;/p&gt;
&lt;h3&gt;The Hardware: AMD AI Max+ 395&lt;/h3&gt;
&lt;p&gt;AMD's Strix Halo architecture represents their latest push into the high-performance APU market, combining powerful CPU cores with substantial integrated graphics. Our test system features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU&lt;/strong&gt;: AMD Ryzen AI Max+ 395 with 16 Zen 5 cores (32 threads)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU&lt;/strong&gt;: Integrated Radeon 8060S (RDNA 3.5 architecture)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory&lt;/strong&gt;: 128GB unified DDR5, configured with 96GB VRAM and 32GB system RAM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute Units&lt;/strong&gt;: 40 CUs dedicated to graphics/compute workloads&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Our test system is the Bosgame M5 AI Mini Desktop, one of the first mini PCs to ship with AMD's Strix Halo silicon. The &lt;a href="https://baud.rs/gmVPEI"&gt;GMKtec EVO-X2&lt;/a&gt; is an extremely similar system if you're looking to replicate this setup. The unified memory architecture is particularly relevant for machine learning workloads. Unlike discrete GPUs with their own VRAM, the Radeon 8060S shares system memory with the CPU. This means no PCIe bottleneck for data transfers, and with 96GB allocated as VRAM, even large models fit comfortably.&lt;/p&gt;
&lt;p&gt;For our TTS workload, the 8060S provides adequate performance. The 1.7B parameter model fits comfortably in memory, and inference runs entirely on GPU once loaded. We see 100% GPU utilization during speech synthesis, indicating the hardware is being fully leveraged.&lt;/p&gt;
&lt;h3&gt;Setting Up the Environment&lt;/h3&gt;
&lt;p&gt;The first challenge with AMD GPUs is getting PyTorch working correctly with ROCm, AMD's open-source GPU compute stack. The Strix Halo uses a newer GPU architecture (gfx1151) that requires ROCm 6.x and some environment variable overrides.&lt;/p&gt;
&lt;h4&gt;Step 1: Create a Python Virtual Environment&lt;/h4&gt;
&lt;p&gt;We'll use a dedicated virtual environment to isolate our TTS dependencies:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;~/qwen-tts
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/qwen-tts
python3&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;venv&lt;span class="w"&gt; &lt;/span&gt;venv
&lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;venv/bin/activate
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Step 2: Install PyTorch with ROCm Support&lt;/h4&gt;
&lt;p&gt;The standard PyTorch installation won't work; we need the ROCm-enabled build. As of this writing, ROCm 6.4 is the latest stable release:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;torch&lt;span class="w"&gt; &lt;/span&gt;torchvision&lt;span class="w"&gt; &lt;/span&gt;torchaudio&lt;span class="w"&gt; &lt;/span&gt;--index-url&lt;span class="w"&gt; &lt;/span&gt;https://download.pytorch.org/whl/rocm6.4
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This downloads PyTorch builds compiled specifically for AMD GPUs. The installation is larger than the standard CUDA builds due to the different compute libraries involved.&lt;/p&gt;
&lt;h4&gt;Step 3: Install Qwen-TTS&lt;/h4&gt;
&lt;p&gt;With PyTorch in place, install the Qwen TTS package:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;qwen-tts&lt;span class="w"&gt; &lt;/span&gt;soundfile&lt;span class="w"&gt; &lt;/span&gt;numpy
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;soundfile&lt;/code&gt; library handles WAV file I/O, while &lt;code&gt;numpy&lt;/code&gt; is needed for audio array manipulation.&lt;/p&gt;
&lt;h4&gt;Step 4: Install xformers for ROCm (Optional but Recommended)&lt;/h4&gt;
&lt;p&gt;The xformers library provides optimized attention implementations that can improve performance:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;xformers&lt;span class="w"&gt; &lt;/span&gt;--index-url&lt;span class="w"&gt; &lt;/span&gt;https://download.pytorch.org/whl/rocm6.4
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;While Qwen-TTS will work without xformers, having it available enables more efficient memory-attention patterns during inference.&lt;/p&gt;
&lt;h4&gt;Step 5: Configure Environment Variables&lt;/h4&gt;
&lt;p&gt;The Strix Halo's gfx1151 architecture isn't explicitly recognized by all ROCm components yet. We need to tell the system to treat it as a compatible architecture:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;HSA_OVERRIDE_GFX_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;11&lt;/span&gt;.0.0
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;GPU_MAX_ALLOC_PERCENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;GPU_MAX_HEAP_SIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's break down what these do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HSA_OVERRIDE_GFX_VERSION=11.0.0&lt;/strong&gt;: Tells the HSA runtime to report the GPU as gfx1100, which has broader library support&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU_MAX_ALLOC_PERCENT=100&lt;/strong&gt;: Allows the GPU to use up to 100% of available memory for allocations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU_MAX_HEAP_SIZE=100&lt;/strong&gt;: Similar memory allocation setting for heap operations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1&lt;/strong&gt;: Enables experimental efficient attention implementations for AMD GPUs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add these to your &lt;code&gt;.bashrc&lt;/code&gt; or create an activation script for convenience.&lt;/p&gt;
&lt;h4&gt;Step 6: Verify GPU Detection&lt;/h4&gt;
&lt;p&gt;Before proceeding, confirm PyTorch can see your GPU:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"CUDA available: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Device count: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device_count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Device name: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_device_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You should see output like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;CUDA available: True
Device count: 1
Device name: AMD Radeon 8060S
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that PyTorch uses "CUDA" terminology even for AMD GPUs when using ROCm; this is for API compatibility.&lt;/p&gt;
&lt;h3&gt;Basic TTS Usage&lt;/h3&gt;
&lt;p&gt;With the environment configured, let's test basic speech synthesis:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;qwen_tts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Qwen3TTSModel&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;soundfile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sf&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;

&lt;span class="c1"&gt;# Load model on GPU with bfloat16 precision&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Qwen3TTSModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attn_implementation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'sdpa'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;device_map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'cuda:0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Check available speakers&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Available speakers: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_supported_speakers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Generate speech&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hello, and welcome to TinyComputers. Today we're exploring text-to-speech on AMD hardware."&lt;/span&gt;
&lt;span class="n"&gt;audios&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate_custom_voice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;speaker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'eric'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Save to file&lt;/span&gt;
&lt;span class="n"&gt;sf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'output.wav'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audios&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Saved audio at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sample_rate&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Hz"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A few important notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We use &lt;code&gt;attn_implementation='sdpa'&lt;/code&gt; for scaled dot-product attention, which works on ROCm&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;device_map='cuda:0'&lt;/code&gt; explicitly places the model on the GPU&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;dtype=torch.bfloat16&lt;/code&gt; reduces memory usage while maintaining quality&lt;/li&gt;
&lt;li&gt;The language parameter must be the full word &lt;code&gt;'english'&lt;/code&gt;, not the abbreviation &lt;code&gt;'en'&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Building a Blog-to-Speech Pipeline&lt;/h3&gt;
&lt;p&gt;For our use case (generating audio versions of blog posts) we need more than basic TTS. Blog posts contain markdown formatting, code blocks, images, and other elements that shouldn't be read aloud. We built a complete pipeline that handles these challenges.&lt;/p&gt;
&lt;h4&gt;The Blog Cleaner&lt;/h4&gt;
&lt;p&gt;Our cleaning process strips out non-spoken content while preserving the narrative flow:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;clean_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Remove YAML frontmatter&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'---'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'---'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

    &lt;span class="c1"&gt;# Strip HTML tags (audio, video, images)&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;audio[^&amp;gt;]*&amp;gt;[\s\S]*?&amp;lt;/audio&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;video[^&amp;gt;]*&amp;gt;[\s\S]*?&amp;lt;/video&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;img[^&amp;gt;]*/?&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;[^&amp;gt;]+&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Remove markdown images and convert links to just text&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'!\[[^\]]*\]\([^)]+\)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'\[([^\]]+)\]\([^)]+\)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'\1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Remove code blocks&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'```[\s\S]*?```'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'`[^`]+`'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Convert headers to sentences&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'^(#{1,6})\s+(.+)$'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'\2.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MULTILINE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Remove emphasis markers&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'\*\*([^*]+)\*\*'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'\1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'\*([^*]+)\*'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'\1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Unit Conversion for Speech&lt;/h4&gt;
&lt;p&gt;Technical content often includes abbreviations that sound awkward when read literally. We convert common units to their spoken forms:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;convert_units_for_speech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'(\d+)\s*GB\b'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'\1 gigabytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'(\d+)\s*MB\b'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'\1 megabytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'(\d+)\s*GHz\b'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'\1 gigahertz'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'(\d+)\s*MHz\b'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'\1 megahertz'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'(\d+)\s*KB\b'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'\1 kilobytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Chunking Long Content&lt;/h4&gt;
&lt;p&gt;TTS models work best with moderate-length inputs. Very long passages can cause quality degradation or memory issues. We split content into chunks at sentence boundaries:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_chars&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;sentences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'(?&amp;lt;=[.!?])\s+'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sentence&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_chars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;sentence&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;" "&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sentence&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;" "&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;The Complete Script&lt;/h4&gt;
&lt;p&gt;Putting it all together, here's our &lt;code&gt;blog_to_speech.py&lt;/code&gt; script:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="ch"&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pathlib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;qwen_tts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Qwen3TTSModel&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;soundfile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sf&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;numpy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;clean_blog_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# Apply cleaning functions...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cleaned_text&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;synthesize_speech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;speaker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"eric"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Qwen3TTSModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;attn_implementation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'sdpa'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;device_map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'cuda:0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;all_audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Processing chunk &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;audios&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate_custom_voice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;speaker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;speaker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;all_audio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audios&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;combined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;concatenate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_audio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Saved &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.1f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s audio to: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'__main__'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'source'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Blog post markdown file'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'-o'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'--output'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'output.wav'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--speaker'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'eric'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clean_blog_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;synthesize_speech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;speaker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Choosing the Right Speaker Voice&lt;/h3&gt;
&lt;p&gt;Qwen3-TTS ships with nine pre-trained speaker voices, each with distinct characteristics:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Speaker&lt;/th&gt;
&lt;th&gt;Characteristics&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Eric&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clear, professional male voice with measured pacing&lt;/td&gt;
&lt;td&gt;Technical content, tutorials, documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aiden&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Younger male voice, slightly more casual&lt;/td&gt;
&lt;td&gt;Blog posts, conversational content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dylan&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deeper male voice with authoritative tone&lt;/td&gt;
&lt;td&gt;Formal presentations, announcements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ryan&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Energetic male voice&lt;/td&gt;
&lt;td&gt;Marketing content, product demos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Serena&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clear female voice, professional&lt;/td&gt;
&lt;td&gt;Corporate content, tutorials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vivian&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Warm female voice&lt;/td&gt;
&lt;td&gt;Storytelling, narrative content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ono Anna&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Female voice with distinct character&lt;/td&gt;
&lt;td&gt;Creative content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sohee&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Female voice, versatile&lt;/td&gt;
&lt;td&gt;General purpose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Uncle Fu&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Character voice&lt;/td&gt;
&lt;td&gt;Specialized applications&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For our technical blog content, we primarily use Eric. His clear enunciation and measured pacing work well for complex technical explanations. The voice handles acronyms, numbers, and technical terminology naturally, making it ideal for content about hardware, programming, and system administration.&lt;/p&gt;
&lt;p&gt;You can easily switch voices by changing the &lt;code&gt;speaker&lt;/code&gt; parameter:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;audios&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate_custom_voice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;speaker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'serena'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Try different voices&lt;/span&gt;
    &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Consider matching voice characteristics to content type. A hardware review might work better with Eric's authoritative tone, while a personal essay might benefit from Aiden's more conversational style.&lt;/p&gt;
&lt;h3&gt;Comparing TTS Options&lt;/h3&gt;
&lt;p&gt;Before settling on Qwen3-TTS, we evaluated several alternatives. Here's how they compare for our use case:&lt;/p&gt;
&lt;h4&gt;Cloud Services&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Amazon Polly&lt;/strong&gt; and &lt;strong&gt;Google Cloud TTS&lt;/strong&gt; offer excellent quality with minimal setup. However, costs accumulate quickly for long-form content. At roughly \$4-16 per million characters (depending on voice quality), a 3000-word blog post costs \$0.10-0.40 per generation. For a site with dozens of posts requiring periodic regeneration, this adds up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ElevenLabs&lt;/strong&gt; produces arguably the most natural voices available, with impressive emotional range. But their pricing model (based on character quotas) makes it expensive for regular content generation. The quality is exceptional, but overkill for straightforward narration.&lt;/p&gt;
&lt;h4&gt;Local Alternatives&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Coqui TTS&lt;/strong&gt; (now deprecated) was a popular open-source option but development has stalled. &lt;strong&gt;Bark&lt;/strong&gt; from Suno produces impressive results but runs slowly and lacks fine-grained control. &lt;strong&gt;XTTS&lt;/strong&gt; offers voice cloning but requires more setup and compute resources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Piper&lt;/strong&gt; deserves special mention as a lightweight option. It runs quickly even on CPU and produces acceptable quality for many applications. However, the voices sound noticeably synthetic compared to Qwen3-TTS, fine for notifications or short snippets, but fatiguing for 30-minute narrations.&lt;/p&gt;
&lt;p&gt;Qwen3-TTS hits a sweet spot: quality approaching cloud services, reasonable compute requirements, and fully local operation. The 1.7B parameter model is large enough for natural prosody but small enough to run on consumer hardware.&lt;/p&gt;
&lt;h3&gt;Batch Processing for Multiple Posts&lt;/h3&gt;
&lt;p&gt;When generating audio for multiple blog posts, efficiency matters. Loading the model takes 15-30 seconds, so we keep it loaded while processing multiple files:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="ch"&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;span class="sd"&gt;"""Batch TTS processing for multiple blog posts"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pathlib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;qwen_tts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Qwen3TTSModel&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;soundfile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sf&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;numpy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;np&lt;/span&gt;

&lt;span class="c1"&gt;# Load model once&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Loading model..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Qwen3TTSModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attn_implementation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'sdpa'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;device_map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'cuda:0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'post1.md'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'post2.md'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'post3.md'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Processing: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clean_blog_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_tts.wav"&lt;/span&gt;

    &lt;span class="c1"&gt;# Process chunks&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;all_audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"  Chunk &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;audios&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate_custom_voice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;speaker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'eric'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;all_audio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audios&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;combined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;concatenate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_audio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Saved: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This approach processes our five-post backlog overnight, with results ready for review in the morning.&lt;/p&gt;
&lt;h3&gt;Performance Characteristics&lt;/h3&gt;
&lt;p&gt;On the AI Max+ 395, speech synthesis runs at roughly real-time to 0.5x real-time speed, meaning a 30-minute audio file takes 30-60 minutes to generate. This is slower than high-end discrete GPUs but perfectly acceptable for batch processing.&lt;/p&gt;
&lt;p&gt;For reference, here's how different content lengths performed in our testing:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;th&gt;Characters&lt;/th&gt;
&lt;th&gt;Chunks&lt;/th&gt;
&lt;th&gt;Audio Duration&lt;/th&gt;
&lt;th&gt;Generation Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Short post&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;td&gt;~15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium post&lt;/td&gt;
&lt;td&gt;15,000&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;~15 min&lt;/td&gt;
&lt;td&gt;~45 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long post&lt;/td&gt;
&lt;td&gt;25,000&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;~27 min&lt;/td&gt;
&lt;td&gt;~90 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Very long&lt;/td&gt;
&lt;td&gt;40,000&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;~45 min&lt;/td&gt;
&lt;td&gt;~150 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The relationship between content length and generation time is roughly linear after the initial model warmup.&lt;/p&gt;
&lt;p&gt;Some observations from our testing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;First chunk latency&lt;/strong&gt;: The first chunk takes longer due to GPU kernel compilation and caching&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory usage&lt;/strong&gt;: Peak usage around 8-10GB during inference&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU utilization&lt;/strong&gt;: Consistent 100% during active synthesis&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quality&lt;/strong&gt;: Indistinguishable from cloud TTS services for most content&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The MIOpen library sometimes logs workspace warnings during execution. These don't affect output quality and can be safely ignored:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;MIOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HIP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IsEnoughWorkspace&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Solver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GemmFwdRest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;103133184&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Integrating Audio into Blog Posts&lt;/h3&gt;
&lt;p&gt;Once we have the WAV file, we convert to MP3 for web delivery and embed an HTML5 audio player:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;ffmpeg&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;blog_post.wav&lt;span class="w"&gt; &lt;/span&gt;-codec:a&lt;span class="w"&gt; &lt;/span&gt;libmp3lame&lt;span class="w"&gt; &lt;/span&gt;-qscale:a&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;blog_post.mp3
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For reviewing TTS output quality, we recommend using &lt;a href="https://baud.rs/tn2v8w"&gt;studio monitor headphones&lt;/a&gt; that reveal any artifacts or unnatural tones in the generated speech.&lt;/p&gt;
&lt;p&gt;The player HTML is straightforward:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"background: #f8f9fa; border: 1px solid #e9ecef;&lt;/span&gt;
&lt;span class="s"&gt;            border-radius: 8px; padding: 16px 20px; margin: 20px 0;"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"audio-widget-header"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"audio-widget-icon"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;🎧&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"color: #495057; font-weight: 600;"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Listen to this article&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;audio&lt;/span&gt; &lt;span class="na"&gt;controls&lt;/span&gt; &lt;span class="na"&gt;preload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"metadata"&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"width: 100%;"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;source&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/audio/blog_post.mp3"&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"audio/mpeg"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"audio-widget-footer"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    27 min · AI-generated narration
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Why We're Doing This&lt;/h3&gt;
&lt;p&gt;Adding audio narration to blog posts serves multiple purposes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Accessibility&lt;/strong&gt;: Readers with visual impairments or reading difficulties can consume content aurally&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Convenience&lt;/strong&gt;: Listeners can enjoy posts during commutes, workouts, or other activities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Engagement&lt;/strong&gt;: Audio content creates a more personal connection with the audience&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reach&lt;/strong&gt;: Some audiences prefer audio format, expanding our potential readership&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Running TTS locally rather than using cloud services gives us:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cost control&lt;/strong&gt;: No per-character or per-minute fees&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy&lt;/strong&gt;: Content never leaves our infrastructure&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistency&lt;/strong&gt;: Same voice and quality across all posts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;: Full control over processing pipeline&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Troubleshooting Common Issues&lt;/h3&gt;
&lt;h4&gt;"CUDA not available" despite GPU present&lt;/h4&gt;
&lt;p&gt;Ensure you've installed the ROCm version of PyTorch, not the standard build:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;pip&lt;span class="w"&gt; &lt;/span&gt;uninstall&lt;span class="w"&gt; &lt;/span&gt;torch&lt;span class="w"&gt; &lt;/span&gt;torchvision&lt;span class="w"&gt; &lt;/span&gt;torchaudio
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;torch&lt;span class="w"&gt; &lt;/span&gt;torchvision&lt;span class="w"&gt; &lt;/span&gt;torchaudio&lt;span class="w"&gt; &lt;/span&gt;--index-url&lt;span class="w"&gt; &lt;/span&gt;https://download.pytorch.org/whl/rocm6.4
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Model runs on CPU instead of GPU&lt;/h4&gt;
&lt;p&gt;Check that &lt;code&gt;device_map='cuda:0'&lt;/code&gt; is specified when loading the model. Also verify the environment variables are set before starting Python.&lt;/p&gt;
&lt;h4&gt;"Unsupported language 'en'"&lt;/h4&gt;
&lt;p&gt;Use the full language name: &lt;code&gt;language='english'&lt;/code&gt; not &lt;code&gt;language='en'&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Out of memory errors&lt;/h4&gt;
&lt;p&gt;Try reducing chunk size or using a smaller batch. The model should fit in 16GB, but very long chunks can spike memory usage.&lt;/p&gt;
&lt;h4&gt;Slow first chunk&lt;/h4&gt;
&lt;p&gt;This is normal; ROCm compiles GPU kernels on first use. Subsequent chunks process faster.&lt;/p&gt;
&lt;h3&gt;Future Improvements&lt;/h3&gt;
&lt;p&gt;Our current pipeline works well but has room for enhancement. Some improvements we're considering:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Voice cloning&lt;/strong&gt;: Qwen3-TTS supports custom voice training. With sufficient audio samples, we could create a unique voice for TinyComputers rather than using the stock speakers. This would provide brand consistency and differentiation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automatic post detection&lt;/strong&gt;: Currently we manually select posts for TTS generation. A CI/CD integration could automatically generate audio for new posts when they're published, keeping the audio library current without manual intervention.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chapter markers&lt;/strong&gt;: For longer posts, embedding chapter markers in the audio file would allow listeners to skip to specific sections. This requires parsing the markdown headers and mapping them to audio timestamps.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multiple format export&lt;/strong&gt;: Beyond MP3, offering Opus or AAC formats could reduce file sizes while maintaining quality, benefiting listeners on metered connections.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Speed adjustment&lt;/strong&gt;: Some listeners prefer 1.25x or 1.5x playback speed. Pre-generating speed-adjusted versions could provide better quality than real-time speed adjustment in the browser.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Running Qwen3-TTS on AMD's Strix Halo platform demonstrates that high-quality local TTS is now accessible beyond NVIDIA hardware. While setup requires some ROCm-specific configuration, the results are impressive: natural-sounding narration suitable for professional content.&lt;/p&gt;
&lt;p&gt;The democratization of AI capabilities continues apace. What once required expensive cloud subscriptions or high-end NVIDIA GPUs now runs on integrated graphics. The AI Max+ 395's Radeon 8060S, primarily designed for gaming and general compute tasks, handles a 1.7-billion parameter language model without breaking a sweat.&lt;/p&gt;
&lt;p&gt;We're actively using this pipeline to generate audio versions of posts across TinyComputers, making our technical content more accessible and convenient for our readers. As of this writing, we've processed our retrocomputing series, hardware reviews, and technical tutorials, dozens of hours of content generated entirely on local hardware.&lt;/p&gt;
&lt;p&gt;The combination of AMD's capable integrated graphics and Qwen's excellent TTS model proves that you don't need expensive discrete GPUs or cloud subscriptions to achieve broadcast-quality speech synthesis. For content creators, educators, and accessibility advocates, this opens new possibilities for enriching written content with audio without ongoing service costs.&lt;/p&gt;
&lt;p&gt;If you're running AMD hardware and want to add audio narration to your own content, this guide should get you started. The initial setup investment pays dividends in ongoing cost savings and the satisfaction of running capable AI models entirely on your own infrastructure. And if you encounter issues along the way, the troubleshooting section above addresses the most common pitfalls we discovered during our own setup process.&lt;/p&gt;
&lt;p&gt;The audio player at the top of many TinyComputers posts now represents a small but meaningful step toward making technical content more accessible. Every post you can listen to while commuting, exercising, or doing dishes is content that might otherwise go unread. That's the real value of local TTS: not just cost savings, but expanded reach for the ideas we share.&lt;/p&gt;</description><category>ai max+ 395</category><category>amd</category><category>audio</category><category>machine learning</category><category>pytorch</category><category>qwen</category><category>rocm</category><category>strix halo</category><category>text-to-speech</category><category>tts</category><guid>https://tinycomputers.io/posts/qwen-tts-on-amd-strix-halo.html</guid><pubDate>Sat, 24 Jan 2026 18:00:00 GMT</pubDate></item><item><title>A Bespoke LLM Code Scanner</title><link>https://tinycomputers.io/posts/a-bespoke-llm-code-scanner.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;h2&gt;Building a Nightly AI Code Scanner with vLLM, ROCm, and JIRA Integration&lt;/h2&gt;
&lt;p&gt;I've been running a ballistics calculation engine, a Rust physics library with several components, like a Flask app wrapper with machine learning capabilities, bindings for a python library as well as a Ruby gem library.  There are also Android and iOS apps, too.  The codebase has grown to about 15,000 lines of Rust and another 10,000 lines of Python. At this scale, bugs hide in edge cases: division by zero, floating-point precision issues in transonic drag calculations, unwrap() panics on unexpected input. &lt;/p&gt;
&lt;p&gt;What if I could run an AI code reviewer every night while I sleep? Not a cloud API with per-token billing that could run up a $500 bill scanning 50 files, but a local model running on my own hardware, grinding through the codebase and filing JIRA tickets for anything suspicious.&lt;/p&gt;
&lt;p&gt;This is the story of building that system.&lt;/p&gt;
&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/a-bespoke-llm-code-scanner_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;17 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;The Hardware: AMD Strix Halo on ROCm 7.0&lt;/h3&gt;
&lt;p&gt;I'm running this on a server with an AMD Radeon 8060S (Strix Halo APU), specifically the &lt;code&gt;gfx1151&lt;/code&gt; architecture. This isn't a data center GPU. It's essentially an integrated GPU with 128GB of shared memory; configured to give 96GB to VRAM and the rest to system RAM. Not the 80GB of HBM3 you'd get on an H100, but enough to run a 32B parameter model comfortably.&lt;/p&gt;
&lt;p&gt;The key insight: for batch processing where latency doesn't matter, you don't need bleeding-edge hardware. A nightly scan can take hours. I'm not serving production traffic; I'm analyzing code files one at a time with a 30-second cooldown between requests. The APU handles this fine.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Hardware Configuration:
&lt;span class="k"&gt;-&lt;/span&gt; AMD Radeon 8060S (gfx1151 Strix Halo APU)
&lt;span class="k"&gt;-&lt;/span&gt; 96GB shared memory
&lt;span class="k"&gt;-&lt;/span&gt; ROCm 7.0 with HSA_OVERRIDE_GFX_VERSION=11.5.1
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;HSA_OVERRIDE_GFX_VERSION&lt;/code&gt; environment variable is critical. Without it, ROCm doesn't recognize the Strix Halo architecture. This is the kind of sharp edge you hit running ML on AMD consumer hardware.&lt;/p&gt;
&lt;h3&gt;Model Selection: Qwen2.5-Coder-7B-Instruct&lt;/h3&gt;
&lt;p&gt;I tested several models:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Quality&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-Coder-V2-Lite&lt;/td&gt;
&lt;td&gt;16B&lt;/td&gt;
&lt;td&gt;32k&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Requires flash_attn (ROCm issues)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-Coder-30B&lt;/td&gt;
&lt;td&gt;30B&lt;/td&gt;
&lt;td&gt;32k&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Too slow on APU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen2.5-Coder-7B-Instruct&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;16k&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Sweet spot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TinyLlama-1.1B&lt;/td&gt;
&lt;td&gt;1.1B&lt;/td&gt;
&lt;td&gt;4k&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;td&gt;Too small for code review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Qwen2.5-Coder-7B-Instruct hits the sweet spot. It understands Rust and Python well enough to spot real issues, runs fast enough to process 50 files per night, and doesn't require flash attention (which has ROCm compatibility issues on consumer hardware).&lt;/p&gt;
&lt;h3&gt;vLLM Setup&lt;/h3&gt;
&lt;p&gt;vLLM provides an OpenAI-compatible API server that makes integration trivial. Here's the startup command:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/vllm-rocm7-venv/bin/activate
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;HSA_OVERRIDE_GFX_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;11&lt;/span&gt;.5.1
python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;vllm.entrypoints.openai.api_server&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--model&lt;span class="w"&gt; &lt;/span&gt;Qwen/Qwen2.5-Coder-7B-Instruct&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--host&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0.0.0&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--trust-remote-code&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--max-model-len&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;16384&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--gpu-memory-utilization&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.85
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;--max-model-len 16384&lt;/code&gt; limits context to 16k tokens. My code files rarely exceed 500 lines (truncated), so this is plenty. The &lt;code&gt;--gpu-memory-utilization 0.85&lt;/code&gt; leaves headroom for the system.&lt;/p&gt;
&lt;p&gt;I run this in a Python venv rather than Docker because ROCm device passthrough with Docker on Strix Halo is finicky. Sometimes you have to choose pragmatism over elegance.&lt;/p&gt;
&lt;h3&gt;Docker Configuration (When It Works)&lt;/h3&gt;
&lt;p&gt;For reference, here's the Docker Compose configuration I initially built. It works on dedicated AMD GPUs but has issues on integrated APUs:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;vllm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;rocm/vllm-dev:latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;vllm-code-scanner&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/dev/kfd:/dev/kfd&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/dev/dri:/dev/dri&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;group_add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;video&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;render&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;security_opt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;seccomp:unconfined&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;cap_add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;SYS_PTRACE&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ipc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;host&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;HSA_OVERRIDE_GFX_VERSION=11.5.1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;PYTORCH_ROCM_ARCH=gfx1151&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;HIP_VISIBLE_DEVICES=0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/home/alex/models:/models&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/home/alex/.cache/huggingface:/root/.cache/huggingface&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"8000:8000"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;python -m vllm.entrypoints.openai.api_server&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;--model Qwen/Qwen2.5-Coder-7B-Instruct&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;--host 0.0.0.0&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;--port 8000&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;--trust-remote-code&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;--max-model-len 16384&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;--gpu-memory-utilization 0.85&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CMD"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"curl"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-f"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"http://localhost:8000/health"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;30s&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10s&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;5&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;start_period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;120s&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;scanner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;code-scanner-agent&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;vllm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;service_healthy&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;VLLM_HOST=vllm&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;VLLM_PORT=8000&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;JIRA_EMAIL=${JIRA_EMAIL}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;JIRA_API_KEY=${JIRA_API_KEY}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/home/alex/projects:/projects:ro&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;./config:/app/config:ro&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/home/alex/projects/code-scanner-results:/app/results&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;ipc: host&lt;/code&gt; and &lt;code&gt;seccomp:unconfined&lt;/code&gt; are necessary for ROCm to function properly. The &lt;code&gt;depends_on&lt;/code&gt; with &lt;code&gt;service_healthy&lt;/code&gt; ensures the scanner waits for vLLM to be fully loaded before starting, which is important since model loading can take 2-3 minutes.&lt;/p&gt;
&lt;p&gt;The scanner Dockerfile is minimal:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.11-slim&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/app&lt;/span&gt;

&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;update&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;ripgrep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;/var/lib/apt/lists/*

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;requirements.txt&lt;span class="w"&gt; &lt;/span&gt;.
&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;--no-cache-dir&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;requirements.txt

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;agent/&lt;span class="w"&gt; &lt;/span&gt;/app/agent/
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;prompts/&lt;span class="w"&gt; &lt;/span&gt;/app/prompts/
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;config/&lt;span class="w"&gt; &lt;/span&gt;/app/config/

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agent.scanner"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Including &lt;code&gt;ripgrep&lt;/code&gt; in the container enables fast pattern matching when the scanner needs to search for related code.&lt;/p&gt;
&lt;h3&gt;The Scanner Architecture&lt;/h3&gt;
&lt;p&gt;The system has three main components:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Systemd       │     │    vLLM         │     │     JIRA        │
│   Timer         │────▶│    Server       │────▶│     API         │
│   (11pm daily)  │     │  (Qwen 7B)      │     │   (tickets)     │
└─────────────────┘     └─────────────────┘     └─────────────────┘
                               │
                               ▼
                    ┌─────────────────────┐
                    │   Scanner Agent     │
                    │ - File discovery    │
                    │ - Code analysis     │
                    │ - Finding validation│
                    │ - JIRA integration  │
                    └─────────────────────┘
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Configuration&lt;/h4&gt;
&lt;p&gt;Everything is driven by a YAML configuration file:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;vllm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"10.1.1.27"&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;8000&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Qwen/Qwen2.5-Coder-7B-Instruct"&lt;/span&gt;

&lt;span class="nt"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;start_hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;23&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# 11pm&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;end_hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;6&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;# 6am&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;max_iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;50&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;cooldown_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;30&lt;/span&gt;

&lt;span class="nt"&gt;repositories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"ballistics-engine"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"/home/alex/projects/ballistics-engine"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"rust"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;scan_patterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"src//*.rs"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;exclude_patterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"target/"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"*.lock"&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"ballistics-api"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"/home/alex/projects/ballistics-api"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"python"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"rust"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;scan_patterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"ballistics//*.py"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"ballistics_rust/src//*.rs"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;exclude_patterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"__pycache__/"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"target/"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;".venv/"&lt;/span&gt;

&lt;span class="nt"&gt;jira&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;project_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"MBA"&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;confidence_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;0.75&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"ai-detected"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"code-scanner"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;max_tickets_per_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;review_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;5&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;confidence_threshold: 0.75&lt;/code&gt; is crucial. Without it, the model reports every minor style issue. At 75%, it focuses on things it's genuinely concerned about.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;review_threshold: 5&lt;/code&gt; triggers a different behavior: if the model finds more than 5 issues, it creates a single summary ticket for manual review rather than flooding JIRA with individual tickets. This is a safety valve for when the model goes haywire.&lt;/p&gt;
&lt;h3&gt;Structured Outputs with Pydantic&lt;/h3&gt;
&lt;p&gt;LLMs are great at finding issues but terrible at formatting output consistently. Left to their own devices, they'll return findings as markdown, prose, JSON with missing fields, or creative combinations thereof.&lt;/p&gt;
&lt;p&gt;The solution is structured outputs. I define Pydantic models for exactly what I expect:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;CRITICAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"critical"&lt;/span&gt;
    &lt;span class="n"&gt;HIGH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"high"&lt;/span&gt;
    &lt;span class="n"&gt;MEDIUM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"medium"&lt;/span&gt;
    &lt;span class="n"&gt;LOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"low"&lt;/span&gt;
    &lt;span class="n"&gt;INFO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"info"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;FindingType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;BUG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"bug"&lt;/span&gt;
    &lt;span class="n"&gt;PERFORMANCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"performance"&lt;/span&gt;
    &lt;span class="n"&gt;SECURITY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"security"&lt;/span&gt;
    &lt;span class="n"&gt;CODE_QUALITY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"code_quality"&lt;/span&gt;
    &lt;span class="n"&gt;POTENTIAL_ISSUE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"potential_issue"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;CodeFinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Path to the file"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;line_start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Starting line number"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;line_end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;finding_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FindingType&lt;/span&gt;
    &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Severity&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;code_snippet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;confidence&lt;/code&gt; field is a float between 0 and 1. The model learns to be honest about uncertainty: "I think this might be a bug (0.6)" versus "This is definitely division by zero (0.95)."&lt;/p&gt;
&lt;p&gt;In a perfect world, I'd use vLLM's Outlines integration for guided JSON generation. In practice, I found that prompting Qwen for JSON and parsing the response works reliably:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_analyze_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CodeFinding&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"""Analyze this code for bugs and issues.&lt;/span&gt;

&lt;span class="s2"&gt;File: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;{content}&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;Return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Each&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;must&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;have&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line_start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;finding_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bug"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"performance"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"security"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"code_quality"&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"critical"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"medium"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"low"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb nb-Type"&gt;null&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="n"&gt;If&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="s2"&gt;"""}&lt;/span&gt;
&lt;span class="s2"&gt;    ]&lt;/span&gt;

&lt;span class="s2"&gt;    response = self._call_llm(messages)&lt;/span&gt;

&lt;span class="s2"&gt;    # Parse JSON from response (handles markdown code blocks too)&lt;/span&gt;
&lt;span class="s2"&gt;    if response.strip().startswith('['):&lt;/span&gt;
&lt;span class="s2"&gt;        findings_data = json.loads(response)&lt;/span&gt;
&lt;span class="s2"&gt;    elif '```json' in response:&lt;/span&gt;
&lt;span class="s2"&gt;        json_str = response.split('```json')[1].split('```')[0]&lt;/span&gt;
&lt;span class="s2"&gt;        findings_data = json.loads(json_str)&lt;/span&gt;
&lt;span class="s2"&gt;    elif '[' in response:&lt;/span&gt;
&lt;span class="s2"&gt;        start = response.index('[')&lt;/span&gt;
&lt;span class="s2"&gt;        end = response.rindex(']') + 1&lt;/span&gt;
&lt;span class="s2"&gt;        findings_data = json.loads(response[start:end])&lt;/span&gt;
&lt;span class="s2"&gt;    else:&lt;/span&gt;
&lt;span class="s2"&gt;        return []&lt;/span&gt;

&lt;span class="s2"&gt;    # Validate each finding with Pydantic&lt;/span&gt;
&lt;span class="s2"&gt;    findings = []&lt;/span&gt;
&lt;span class="s2"&gt;    for item in findings_data:&lt;/span&gt;
&lt;span class="s2"&gt;        try:&lt;/span&gt;
&lt;span class="s2"&gt;            finding = CodeFinding(item)&lt;/span&gt;
&lt;span class="s2"&gt;            findings.append(finding)&lt;/span&gt;
&lt;span class="s2"&gt;        except ValidationError:&lt;/span&gt;
&lt;span class="s2"&gt;            pass  # Skip malformed findings&lt;/span&gt;

&lt;span class="s2"&gt;    return findings&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;The System Prompt&lt;/h3&gt;
&lt;p&gt;The system prompt is where you teach the model what you care about. Here's mine:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;You are an expert code reviewer specializing in Rust and Python.
Your job is to find bugs, performance issues, security vulnerabilities,
and code quality problems.

You are analyzing code from a ballistics calculation project that includes:
- A Rust physics engine for trajectory calculations
- Python Flask API with ML models
- PyO3 bindings between Rust and Python

Key areas to focus on:
1. Numerical precision issues (floating point errors, rounding)
2. Edge cases in physics calculations (division by zero, negative values)
3. Memory safety in Rust code
4. Error handling (silent failures, unwrap panics)
5. Performance bottlenecks (unnecessary allocations, redundant calculations)
6. Security issues (input validation, injection vulnerabilities)

Be conservative with findings - only report issues you are confident about.
Avoid false positives.
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The phrase "Be conservative with findings" is doing heavy lifting. Without it, the model reports everything that looks slightly unusual. With it, it focuses on actual problems.&lt;/p&gt;
&lt;h3&gt;Timeout Handling&lt;/h3&gt;
&lt;p&gt;Large files (500+ lines) can take a while to analyze. My initial 120-second timeout caused failures on complex files. I bumped it to 600 seconds (10 minutes):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/chat/completions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I also truncate files to 300 lines. For longer files, the model only sees the first 300 lines. This is a trade-off (I might miss bugs in the back half of long files) but it keeps scans predictable and prevents timeout cascades.  I plan to revisit this in future iterations.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Truncated to 300 lines for analysis"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;JIRA Integration&lt;/h3&gt;
&lt;p&gt;When the scanner finds issues, it creates JIRA tickets automatically. The API is straightforward:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;create_jira_tickets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CodeFinding&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="n"&gt;jira_base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;jira_domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/rest/api/3"&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Map severity to JIRA priority&lt;/span&gt;
        &lt;span class="n"&gt;priority_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CRITICAL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Highest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"High"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEDIUM&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Medium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Low"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Lowest"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s2"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"MBA"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="s2"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"[AI] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"doc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s2"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"paragraph"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;build_description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
                    &lt;span class="p"&gt;]}]&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="s2"&gt;"issuetype"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Bug"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;finding_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;FindingType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BUG&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="s2"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;priority_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
                &lt;span class="s2"&gt;"labels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ai-detected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"code-scanner"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;jira_base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/issue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jira_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jira_api_key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;[AI]&lt;/code&gt; prefix in the summary makes it obvious these tickets came from the scanner. The &lt;code&gt;ai-detected&lt;/code&gt; label allows filtering.&lt;/p&gt;
&lt;p&gt;I add a 2-second delay between ticket creation to avoid rate limiting:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Rate limit protection&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Systemd Scheduling&lt;/h3&gt;
&lt;p&gt;The scanner runs nightly via systemd timer:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# /etc/systemd/system/code-scanner.timer&lt;/span&gt;
&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Run Code Scanner nightly at 11pm&lt;/span&gt;

&lt;span class="k"&gt;[Timer]&lt;/span&gt;
&lt;span class="na"&gt;OnCalendar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;*-*-* 23:00:00&lt;/span&gt;
&lt;span class="na"&gt;Persistent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;RandomizedDelaySec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;300&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;RandomizedDelaySec=300&lt;/code&gt; adds up to 5 minutes of random delay. This prevents the scanner from always starting at exactly 11:00:00, which helps if multiple services share the same schedule.&lt;/p&gt;
&lt;p&gt;The service unit is a oneshot that runs the scanner script:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# /etc/systemd/system/code-scanner.service&lt;/span&gt;
&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Code Scanner Agent&lt;/span&gt;
&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;alex&lt;/span&gt;
&lt;span class="na"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/alex/projects/ballistics/code-scanner&lt;/span&gt;
&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/alex/projects/ballistics/code-scanner/scripts/start_scanner.sh&lt;/span&gt;
&lt;span class="na"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;25200&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;TimeoutStartSec=25200&lt;/code&gt; (7 hours) gives the scanner enough time to complete even if it scans every file.&lt;/p&gt;
&lt;h3&gt;Sample Findings&lt;/h3&gt;
&lt;p&gt;Here's what the scanner actually finds. From a recent run:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"file_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/home/alex/projects/ballistics-engine/src/fast_trajectory.rs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"line_start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;115&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"finding_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Division by zero in fast_integrate when velocity approaches zero"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The division dt / velocity_magnitude could result in division by zero if the projectile stalls (velocity_magnitude = 0). This can happen at the apex of a high-angle shot."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"suggestion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add a check for velocity_magnitude &amp;lt; epsilon before division, or clamp to a minimum value."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is a real issue. In ballistics calculations, a projectile fired at a high angle momentarily has zero horizontal velocity at the apex. Without a guard, this causes a panic.&lt;/p&gt;
&lt;p&gt;Not every finding is valid. The model occasionally flags intentional design decisions as "issues." But at a 75% confidence threshold, the false positive rate is manageable; maybe 1 in 10 findings needs to be closed as "not a bug."&lt;/p&gt;
&lt;h3&gt;Trade-offs and Lessons&lt;/h3&gt;
&lt;p&gt;What works well:
- Finding numerical edge cases (division by zero, overflow)
- Spotting unwrap() calls on Options that might be None
- Identifying missing error handling
- Flagging dead code and unreachable branches&lt;/p&gt;
&lt;p&gt;What doesn't work as well:
- Understanding business logic (the model doesn't know physics)
- Spotting subtle race conditions in concurrent code
- False positives on intentional patterns&lt;/p&gt;
&lt;p&gt;Operational lessons:
- Start with a low iteration limit (10-20 files) to test the pipeline
- Monitor the first few runs manually before trusting it
- Keep credentials in &lt;code&gt;.env&lt;/code&gt; files excluded from rsync
- The 300-line truncation is aggressive; consider chunking for long files&lt;/p&gt;
&lt;h3&gt;Handling JSON Parse Failures&lt;/h3&gt;
&lt;p&gt;Despite asking for JSON, LLMs sometimes produce malformed output. I see two failure modes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Truncated JSON: The model runs out of tokens mid-response, leaving an unterminated string or missing closing brackets.&lt;/li&gt;
&lt;li&gt;Wrapped JSON: The model adds explanatory text around the JSON, like "Here are the findings:" before the array.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;My parser handles both:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;parse_findings_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Extract JSON from potentially messy LLM output."""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Best case: raw JSON array&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'['&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# Fall through to extraction&lt;/span&gt;

    &lt;span class="c1"&gt;# Common case: JSON in markdown code block&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s1"&gt;'```json'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;json_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'```json'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'```'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ne"&gt;IndexError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="c1"&gt;# Fallback: extract JSON array from surrounding text&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s1"&gt;'['&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="s1"&gt;']'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'['&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rindex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;']'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="c1"&gt;# Give up&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Could not extract JSON from response"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When parsing fails, I log the error and skip that file rather than crashing the entire scan. In a typical 50-file run, I see 2-3 parse failures, annoying but acceptable.&lt;/p&gt;
&lt;h3&gt;Testing the Pipeline&lt;/h3&gt;
&lt;p&gt;Before trusting the scanner with JIRA ticket creation, I ran it in "dry run" mode:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Set max iterations low and disable JIRA&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;MAX_ITERATIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="c1"&gt;# In config: jira.enabled: false&lt;/span&gt;

python&lt;span class="w"&gt; &lt;/span&gt;run_scanner_direct.py
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This scans just 5 files and prints findings without creating tickets. I manually reviewed each finding:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;True positive: Division by zero in trajectory calculation, good catch&lt;/li&gt;
&lt;li&gt;False positive: Flagged intentional &lt;code&gt;unwrap()&lt;/code&gt; on a guaranteed-Some Option, needs better context&lt;/li&gt;
&lt;li&gt;True positive: Dead code path never executed, valid cleanup suggestion&lt;/li&gt;
&lt;li&gt;Marginal: Style suggestion about variable naming, below my quality threshold&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After tuning the confidence threshold and system prompt, the true positive rate improved to roughly 90%.&lt;/p&gt;
&lt;h3&gt;Monitoring and Observability&lt;/h3&gt;
&lt;p&gt;The scanner writes detailed logs to stdout and a JSON results file. Sample log output:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CODE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SCANNER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AGENT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;STARTING&lt;/span&gt;
&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Qwen&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Qwen2&lt;/span&gt;&lt;span class="mf"&gt;.5&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Coder&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Instruct&lt;/span&gt;
&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Starting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;scan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;
&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;Found&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;scan&lt;/span&gt;
&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;Scanning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;trajectory_sampling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rs&lt;/span&gt;
&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;Truncated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;analysis&lt;/span&gt;
&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="k"&gt;Found&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LOW&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Redundant&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;step_m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;
&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LOW&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Potential&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;off&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;by&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The JSON results include full finding details:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20251126_151136"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"total_findings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"repositories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ballistics-engine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"files_scanned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"findings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"duration_seconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1842.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"iterations_used"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I keep the last 30 result files (configurable) for historical comparison. Eventually I'll build a dashboard showing finding trends over time.&lt;/p&gt;
&lt;h3&gt;What's Next&lt;/h3&gt;
&lt;p&gt;The current system is batch-oriented: run once per night, file tickets, done. Future improvements I'm considering:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pre-commit integration: Run on changed files only, fast enough for CI&lt;/li&gt;
&lt;li&gt;Retrieval-augmented context: Include related files when analyzing (e.g., when scanning a function, include its callers)&lt;/li&gt;
&lt;li&gt;Learning from feedback: Track which tickets get closed as "not a bug" and use that to tune prompts&lt;/li&gt;
&lt;li&gt;Multi-model ensemble: Run the same code through two models, only file tickets when both agree&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For now, though, the simple approach works. Every morning I check JIRA, triage the overnight findings, and fix the real bugs. The model isn't perfect, but it finds things I miss. And unlike a human reviewer, it never gets tired, never skips files, and never has a bad day.&lt;/p&gt;
&lt;h3&gt;Get the Code&lt;/h3&gt;
&lt;p&gt;I've open-sourced the complete scanner implementation on GitHub: &lt;strong&gt;&lt;a href="https://baud.rs/VzUVjf"&gt;llm-code-scanner&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The project includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dual scanning modes&lt;/strong&gt;: Fast nightly scans via vLLM and comprehensive weekly analyses through Ollama&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smart deduplication&lt;/strong&gt;: SQLite database prevents redundant issue tracking across runs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JIRA integration&lt;/strong&gt;: Automatically creates tickets for findings above your confidence threshold&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Email reports&lt;/strong&gt;: SendGrid integration for daily/weekly summaries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-language support&lt;/strong&gt;: Python, Rust, TypeScript, Kotlin, Swift, Go, and more&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To get started, clone the repo, configure your &lt;code&gt;scanner_config.yaml&lt;/code&gt; with your vLLM/Ollama server details, and run &lt;code&gt;python -m agent.scanner&lt;/code&gt;. The README has full setup instructions including environment variables for JIRA and SendGrid integration.&lt;/p&gt;</description><category>ai</category><category>amd</category><category>automation</category><category>code review</category><category>jira</category><category>llm</category><category>machine learning</category><category>python</category><category>qwen</category><category>rocm</category><category>rust</category><category>strix halo</category><category>vllm</category><guid>https://tinycomputers.io/posts/a-bespoke-llm-code-scanner.html</guid><pubDate>Wed, 26 Nov 2025 16:49:15 GMT</pubDate></item><item><title>AMD GPU Comparison: Max+ 395 vs RX 7900 for LLM Inference</title><link>https://tinycomputers.io/posts/amd-gpu-comparison-max%2B-395-vs-rx-7900-xtx.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;p&gt;This report compares the inference performance of two GPU systems running local LLM models using Ollama. The benchmark tests were conducted using the llm-tester tool with concurrent requests set to 1, simulating single-user workload scenarios.&lt;/p&gt;
&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/amd-gpu-comparison-max+-395-vs-rx-7900-xtx_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;8 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;Test Configuration&lt;/h3&gt;
&lt;h4&gt;Systems Tested&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://baud.rs/WZgnl1"&gt;AI Max+ 395&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Host: bosgame.localnet&lt;/li&gt;
&lt;li&gt;ROCm: Custom installation in home directory&lt;/li&gt;
&lt;li&gt;Memory: 32 GB unified memory&lt;/li&gt;
&lt;li&gt;VRAM: 96 GB&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://baud.rs/CVlNMe"&gt;AMD Radeon RX 7900 XTX&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Host: rig.localnet&lt;/li&gt;
&lt;li&gt;ROCm: System default installation&lt;/li&gt;
&lt;li&gt;Memory: 96 GB&lt;/li&gt;
&lt;li&gt;VRAM: 24 GB&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Models Tested&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/yc2cPE"&gt;deepseek-r1:1.5b&lt;/a&gt; - Small reasoning model (1.1 GB)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/IRySKd"&gt;qwen3:latest&lt;/a&gt; - Latest Qwen 3 model (1.1 GB)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Test Methodology&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Benchmark Tool: llm-tester (https://github.com/Laszlobeer/llm-tester)&lt;/li&gt;
&lt;li&gt;Concurrent Requests: 1 (single-user simulation)&lt;/li&gt;
&lt;li&gt;Tasks per Model: 5 diverse prompts&lt;/li&gt;
&lt;li&gt;Timeout: 180 seconds per task&lt;/li&gt;
&lt;li&gt;Backend: Ollama API (http://localhost:11434)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Performance Results&lt;/h3&gt;
&lt;h4&gt;deepseek-r1:1.5b Performance&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Avg Tokens/s&lt;/th&gt;
&lt;th&gt;Avg Latency&lt;/th&gt;
&lt;th&gt;Total Time&lt;/th&gt;
&lt;th&gt;Performance Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AMD RX 7900&lt;/td&gt;
&lt;td&gt;197.01&lt;/td&gt;
&lt;td&gt;6.54s&lt;/td&gt;
&lt;td&gt;32.72s&lt;/td&gt;
&lt;td&gt;1.78x faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max+ 395&lt;/td&gt;
&lt;td&gt;110.52&lt;/td&gt;
&lt;td&gt;21.51s&lt;/td&gt;
&lt;td&gt;107.53s&lt;/td&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Detailed Results - AMD RX 7900:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Task 1: 196.88 tokens/s, Latency: 9.81s&lt;/li&gt;
&lt;li&gt;Task 2: 185.87 tokens/s, Latency: 17.60s&lt;/li&gt;
&lt;li&gt;Task 3: 200.72 tokens/s, Latency: 1.97s&lt;/li&gt;
&lt;li&gt;Task 4: 200.89 tokens/s, Latency: 1.76s&lt;/li&gt;
&lt;li&gt;Task 5: 200.70 tokens/s, Latency: 1.57s&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Detailed Results - Max+ 395:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Task 1: 111.78 tokens/s, Latency: 13.38s&lt;/li&gt;
&lt;li&gt;Task 2: 93.81 tokens/s, Latency: 82.23s&lt;/li&gt;
&lt;li&gt;Task 3: 115.97 tokens/s, Latency: 3.83s&lt;/li&gt;
&lt;li&gt;Task 4: 114.72 tokens/s, Latency: 4.52s&lt;/li&gt;
&lt;li&gt;Task 5: 116.34 tokens/s, Latency: 3.57s&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="AMD RX 7900 XTX running deepseek-r1:1.5b benchmark" src="https://tinycomputers.io/images/llm-benchmarks/rig-deepseek-r1.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;AMD RX 7900 XTX performance on deepseek-r1:1.5b model&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Max+ 395 running deepseek-r1:1.5b benchmark" src="https://tinycomputers.io/images/llm-benchmarks/bosgame-deepseek-r1.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Max+ 395 performance on deepseek-r1:1.5b model&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;qwen3:latest Performance&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Avg Tokens/s&lt;/th&gt;
&lt;th&gt;Avg Latency&lt;/th&gt;
&lt;th&gt;Total Time&lt;/th&gt;
&lt;th&gt;Performance Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AMD RX 7900&lt;/td&gt;
&lt;td&gt;86.46&lt;/td&gt;
&lt;td&gt;12.81s&lt;/td&gt;
&lt;td&gt;64.04s&lt;/td&gt;
&lt;td&gt;2.71x faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max+ 395&lt;/td&gt;
&lt;td&gt;31.85&lt;/td&gt;
&lt;td&gt;41.00s&lt;/td&gt;
&lt;td&gt;204.98s&lt;/td&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Detailed Results - AMD RX 7900:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Task 1: 86.56 tokens/s, Latency: 15.07s&lt;/li&gt;
&lt;li&gt;Task 2: 85.69 tokens/s, Latency: 18.37s&lt;/li&gt;
&lt;li&gt;Task 3: 86.74 tokens/s, Latency: 7.15s&lt;/li&gt;
&lt;li&gt;Task 4: 87.91 tokens/s, Latency: 1.56s&lt;/li&gt;
&lt;li&gt;Task 5: 85.43 tokens/s, Latency: 21.90s&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Detailed Results - Max+ 395:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Task 1: 32.21 tokens/s, Latency: 33.15s&lt;/li&gt;
&lt;li&gt;Task 2: 27.53 tokens/s, Latency: 104.82s&lt;/li&gt;
&lt;li&gt;Task 3: 33.47 tokens/s, Latency: 16.79s&lt;/li&gt;
&lt;li&gt;Task 4: 34.96 tokens/s, Latency: 4.64s&lt;/li&gt;
&lt;li&gt;Task 5: 31.08 tokens/s, Latency: 45.59s&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="AMD RX 7900 XTX running qwen3:latest benchmark" src="https://tinycomputers.io/images/llm-benchmarks/rig-qwen3.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;AMD RX 7900 XTX performance on qwen3:latest model&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Max+ 395 running qwen3:latest benchmark" src="https://tinycomputers.io/images/llm-benchmarks/bosgame-qwen3.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Max+ 395 performance on qwen3:latest model&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Comparative Analysis&lt;/h3&gt;
&lt;h4&gt;Overall Performance Summary&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;RX 7900&lt;/th&gt;
&lt;th&gt;Max+ 395&lt;/th&gt;
&lt;th&gt;Performance Multiplier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;deepseek-r1:1.5b&lt;/td&gt;
&lt;td&gt;197.01 tok/s&lt;/td&gt;
&lt;td&gt;110.52 tok/s&lt;/td&gt;
&lt;td&gt;1.78x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:latest&lt;/td&gt;
&lt;td&gt;86.46 tok/s&lt;/td&gt;
&lt;td&gt;31.85 tok/s&lt;/td&gt;
&lt;td&gt;2.71x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Key Findings&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;RX 7900 Dominance: The AMD RX 7900 significantly outperforms the Max+ 395 across both models&lt;/li&gt;
&lt;li&gt;78% faster on deepseek-r1:1.5b&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;171% faster on qwen3:latest&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Model-Dependent Performance Gap: The performance difference is more pronounced with the larger/more complex model (qwen3:latest), suggesting the RX 7900 handles larger models more efficiently&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Consistency: The RX 7900 shows more consistent performance across tasks, with lower variance in latency&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Total Execution Time:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;For deepseek-r1:1.5b: RX 7900 completed in 32.72s vs 107.53s (3.3x faster)&lt;/li&gt;
&lt;li&gt;For qwen3:latest: RX 7900 completed in 64.04s vs 204.98s (3.2x faster)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Comparison with Previous Results&lt;/h3&gt;
&lt;h4&gt;Desktop PC (i9-9900k + RTX 2080, 8GB VRAM)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;deepseek-r1:1.5b: 143 tokens/s&lt;/li&gt;
&lt;li&gt;qwen3:latest: 63 tokens/s&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;M4 Mac (24GB Unified Memory)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;deepseek-r1:1.5b: 81 tokens/s&lt;/li&gt;
&lt;li&gt;qwen3:latest: Timeout issues (needed 120s timeout)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Performance Ranking&lt;/h4&gt;
&lt;p&gt;deepseek-r1:1.5b:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;AMD RX 7900: 197.01 tok/s ⭐&lt;/li&gt;
&lt;li&gt;RTX 2080 (CUDA): 143 tok/s&lt;/li&gt;
&lt;li&gt;Max+ 395: 110.52 tok/s&lt;/li&gt;
&lt;li&gt;M4 Mac: 81 tok/s&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;qwen3:latest:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;AMD RX 7900: 86.46 tok/s ⭐&lt;/li&gt;
&lt;li&gt;RTX 2080 (CUDA): 63 tok/s&lt;/li&gt;
&lt;li&gt;Max+ 395: 31.85 tok/s&lt;/li&gt;
&lt;li&gt;M4 Mac: Unable to complete within timeout&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Cost-Benefit Analysis&lt;/h3&gt;
&lt;h4&gt;System Pricing Context&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Framework Desktop with Max+ 395: ~$2,500&lt;/li&gt;
&lt;li&gt;AMD RX 7900: Available as standalone GPU (~$600-800 used, ~$900-1000 new)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Value Proposition&lt;/h4&gt;
&lt;p&gt;The AMD RX 7900 delivers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.78-2.71x better performance than the Max+ 395&lt;/li&gt;
&lt;li&gt;Significantly better price-to-performance ratio (~$800 vs $2,500)&lt;/li&gt;
&lt;li&gt;Dedicated GPU VRAM vs shared unified memory&lt;/li&gt;
&lt;li&gt;Better thermal management in desktop form factor&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The $2,500 Framework Desktop investment could alternatively fund:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AMD RX 7900 GPU&lt;/li&gt;
&lt;li&gt;High-performance desktop motherboard&lt;/li&gt;
&lt;li&gt;AMD Ryzen CPU&lt;/li&gt;
&lt;li&gt;32-64GB DDR5 RAM&lt;/li&gt;
&lt;li&gt;Storage and cooling&lt;/li&gt;
&lt;li&gt;With budget remaining&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Conclusions&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Clear Performance Winner: The AMD RX 7900 is substantially faster than the Max+ 395 for LLM inference workloads&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Value Analysis: The Framework Desktop's $2,500 price point doesn't provide competitive performance for LLM workloads compared to desktop alternatives&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use Case Consideration: The Framework Desktop offers portability and unified memory benefits, but if LLM performance is the primary concern, the RX 7900 desktop configuration is superior&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ROCm Compatibility: Both systems successfully ran ROCm workloads, demonstrating AMD's growing ecosystem for AI/ML tasks&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Recommendation: For users prioritizing LLM inference performance per dollar, a desktop workstation with an RX 7900 provides significantly better value than the Max+ 395 Framework Desktop&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Technical Notes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;All tests used identical benchmark methodology with single concurrent requests&lt;/li&gt;
&lt;li&gt;Both systems were running similar ROCm configurations&lt;/li&gt;
&lt;li&gt;Network latency was negligible (local Ollama API)&lt;/li&gt;
&lt;li&gt;Results represent real-world single-user inference scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Systems Information&lt;/h3&gt;
&lt;p&gt;Both systems are running:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Operating System: Linux&lt;/li&gt;
&lt;li&gt;LLM Runtime: Ollama&lt;/li&gt;
&lt;li&gt;Acceleration: ROCm (AMD GPU compute)&lt;/li&gt;
&lt;li&gt;Python: 3.12.3&lt;/li&gt;
&lt;/ul&gt;</description><category>amd gpu</category><category>benchmarks</category><category>deepseek</category><category>llm inference</category><category>machine learning</category><category>max+ 395</category><category>ollama</category><category>performance comparison</category><category>qwen</category><category>rocm</category><category>rx 7900</category><guid>https://tinycomputers.io/posts/amd-gpu-comparison-max%2B-395-vs-rx-7900-xtx.html</guid><pubDate>Tue, 28 Oct 2025 21:11:26 GMT</pubDate></item><item><title>Getting YOLOv8 Training Working on AMD Ryzen™ AI Max+ 395</title><link>https://tinycomputers.io/posts/getting-yolov8-training-working-on-amd-ryzentm-al-max%2B-395.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/getting-yolov8-training-working-on-amd-ryzentm-al-max+-395_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;20 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Machine learning on AMD GPUs has always been... interesting. With NVIDIA's CUDA dominating the landscape, AMD's ROCm platform remains the underdog: powerful, but often requiring patience and persistence to get working properly. This is the story of how I got YOLOv8 object detection training working on an AMD Radeon 8060S integrated GPU (gfx1151) in the AMD RYZEN AI MAX+ 395 after encountering batch normalization failures, version mismatches, and a critical bug in MIOpen.&lt;/p&gt;
&lt;p&gt;The goal was simple: train a bullet hole detection model for a ballistics application using YOLOv8. The journey? Anything but simple.&lt;/p&gt;
&lt;h3&gt;The Hardware&lt;/h3&gt;
&lt;p&gt;System Specifications:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU: AMD RYZEN AI MAX+ 395&lt;/li&gt;
&lt;li&gt;GPU: AMD Radeon 8060S (integrated, RDNA 3.5 architecture, gfx1151)&lt;/li&gt;
&lt;li&gt;VRAM: 96GB shared system memory&lt;/li&gt;
&lt;li&gt;ROCm Version: 7.0.2&lt;/li&gt;
&lt;li&gt;ROCk module: 6.14.14&lt;/li&gt;
&lt;li&gt;PyTorch: 2.8.0+rocm7.0.0.git64359f59&lt;/li&gt;
&lt;li&gt;MIOpen: Initially 3.0.5.1 (version code 3005001), later custom build&lt;/li&gt;
&lt;li&gt;OS: Linux (conda environment: pt2.8-rocm7)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The AMD Radeon 8060S is an integrated GPU in the AMD RYZEN AI MAX+ 395 based on AMD's RDNA 3.5 architecture (gfx1151). What makes this system particularly interesting for machine learning is the massive 96GB of shared system memory available to the GPU, far more VRAM than typical consumer discrete GPUs. While machine learning support on RDNA 3.5 is still maturing compared to older RDNA 2 architectures, the memory capacity makes it compelling for AI workloads.&lt;/p&gt;
&lt;p&gt;But, for about $1,699, you can get up to 96GB of VRAM in a &lt;a href="https://baud.rs/r4rMKO"&gt;whisper-quiet form factor&lt;/a&gt;. This setup beats the pants off of my &lt;a href="https://tinycomputers.io/posts/eights-years-on-the-NVIDIA-tesla-p100-still-delivers-for-budget-artificial-intelligence-work.html"&gt;old GPU rig&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Why YOLOv8 and Ultralytics?&lt;/h3&gt;
&lt;p&gt;Before diving into the technical challenges, it's worth explaining why we chose YOLOv8 from &lt;a href="https://baud.rs/jf4gLA"&gt;Ultralytics&lt;/a&gt; for this project.&lt;/p&gt;
&lt;p&gt;YOLOv8 (You Only Look Once, version 8) is the latest iteration of one of the most popular object detection architectures. Developed and maintained by Ultralytics, it offers several advantages:&lt;/p&gt;
&lt;h4&gt;Why Ultralytics YOLOv8?&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;State-of-the-art Accuracy: YOLOv8 achieves excellent detection accuracy while maintaining real-time inference speeds, critical for practical applications.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ease of Use: Ultralytics provides a clean, well-documented Python API that makes training custom models remarkably straightforward:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;ultralytics&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;YOLO&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;YOLO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"yolov8n.pt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"dataset.yaml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;epochs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Active Development: Ultralytics is actively maintained with frequent updates, bug fixes, and community support. This proved invaluable during debugging.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Model Variants: YOLOv8 comes in multiple sizes (nano, small, medium, large, extra-large), allowing us to balance accuracy vs. speed for our specific use case.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Built-in Data Augmentation: The framework includes extensive data augmentation capabilities out of the box, essential for training robust detection models with limited training data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PyTorch Native: Being built on PyTorch meant it should theoretically work with ROCm (AMD's CUDA equivalent)... in theory.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For our bullet hole detection application, YOLOv8's ability to accurately detect small objects (bullet holes in paper targets) while training efficiently made it the obvious choice. Little did I know that "training efficiently" would require a week-long debugging odyssey.&lt;/p&gt;
&lt;h3&gt;The Initial Setup (ROCm 7.0.0)&lt;/h3&gt;
&lt;p&gt;I started with ROCm 7.0.0, following AMD's official installation guide. Everything installed cleanly:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"import torch; print(torch.cuda.is_available())"&lt;/span&gt;
True

$&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"import torch; print(torch.cuda.get_device_name(0))"&lt;/span&gt;
AMD&lt;span class="w"&gt; &lt;/span&gt;Radeon&lt;span class="w"&gt; &lt;/span&gt;Graphics
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Perfect! PyTorch recognized the GPU. Time to train some models, right?&lt;/p&gt;
&lt;h3&gt;The First Failure: Batch Normalization&lt;/h3&gt;
&lt;p&gt;I loaded a simple YOLOv8 nano model and kicked off training:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;ultralytics&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;YOLO&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;YOLO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"yolov8n.pt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"data/bullet_hole_dataset_combined/data.yaml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;epochs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;imgsz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;416&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cuda:0"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Within seconds, the training crashed:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;RuntimeError&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;miopenStatusUnknownError&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The error was cryptic, but digging deeper revealed the real issue: MIOpen was failing to compile batch normalization kernels with inline assembly errors:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&amp;lt;inline asm&amp;gt;:14:20: error: not a valid operand.
v_add_f32 v4 v4 v4 row_bcast:15 row_mask:0xa
                   ^
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Batch normalization. The most common operation in modern deep learning, and it was failing spectacularly on gfx1151. The inline assembly instructions (&lt;code&gt;row_bcast&lt;/code&gt; and &lt;code&gt;row_mask&lt;/code&gt;) appeared incompatible with the RDNA 3.5 architecture.&lt;/p&gt;
&lt;h4&gt;What is Batch Normalization?&lt;/h4&gt;
&lt;p&gt;Batch normalization (BatchNorm) is a technique that normalizes layer inputs across a mini-batch, helping neural networks train faster and more stably. It's used in virtually every modern CNN architecture, including YOLO.&lt;/p&gt;
&lt;p&gt;The error message pointed to &lt;code&gt;MIOpen&lt;/code&gt;, AMD's equivalent of NVIDIA's cuDNN, a library of optimized deep learning primitives.&lt;/p&gt;
&lt;h3&gt;Attempt 1: Upgrade to ROCm 7.0.2&lt;/h3&gt;
&lt;p&gt;My first instinct was to upgrade ROCm. Version 7.0.0 was relatively new, and perhaps 7.0.2 had fixed the batch normalization issues.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Upgraded PyTorch to ROCm 7.0.2&lt;/span&gt;
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;--upgrade&lt;span class="w"&gt; &lt;/span&gt;torch&lt;span class="w"&gt; &lt;/span&gt;--index-url&lt;span class="w"&gt; &lt;/span&gt;https://download.pytorch.org/whl/rocm7.0
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Result? Same error. Batch normalization still failed.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;RuntimeError&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;miopenStatusUnknownError&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With the same inline assembly compilation errors about invalid &lt;code&gt;row_bcast&lt;/code&gt; and &lt;code&gt;row_mask&lt;/code&gt; operands. At this point, I realized this wasn't a simple version mismatch; there was something fundamentally broken with MIOpen's batch normalization implementation for the gfx1151 architecture.&lt;/p&gt;
&lt;h3&gt;The Revelation: It's MIOpen, Not ROCm&lt;/h3&gt;
&lt;p&gt;After hours of testing different PyTorch versions, driver configurations, and kernel parameters, I turned to the ROCm community for help.&lt;/p&gt;
&lt;p&gt;I posted my issue on &lt;a href="https://baud.rs/N50zpY"&gt;Reddit's r/ROCm subreddit&lt;/a&gt;, describing the inline assembly compilation failures and &lt;code&gt;miopenStatusUnknownError&lt;/code&gt; on gfx1151. Within a few hours, a knowledgeable Redditor responded with a crucial piece of information:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"There's a known issue with MIOpen 3.0.x and gfx1151 batch normalization. The inline assembly instructions use operands that aren't compatible with RDNA 3. A fix was recently merged into the develop branch. Try using a nightly build of MIOpen or build from source."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was the breakthrough I needed. The issue wasn't with ROCm itself or PyTorch; it was specifically MIOpen version 3.0.5.1 that shipped with ROCm 7.0.x. The maintainers had already fixed the gfx1151 batch normalization bug in a recent pull request, but it hadn't made it into a stable release yet.&lt;/p&gt;
&lt;p&gt;The Reddit user suggested two options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use a nightly Docker container with the latest MIOpen build&lt;/li&gt;
&lt;li&gt;Build MIOpen 3.5.1 from source using the develop branch&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Testing the Theory: Docker Nightly Builds&lt;/h3&gt;
&lt;p&gt;Before committing to building from source, I wanted to verify that a newer MIOpen would actually fix the problem. AMD provides nightly Docker images with bleeding-edge ROCm builds:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;docker&lt;span class="w"&gt; &lt;/span&gt;pull&lt;span class="w"&gt; &lt;/span&gt;rocm/pytorch-nightly:latest

docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;--rm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--device&lt;span class="o"&gt;=&lt;/span&gt;/dev/kfd&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--device&lt;span class="o"&gt;=&lt;/span&gt;/dev/dri&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--group-add&lt;span class="w"&gt; &lt;/span&gt;video&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;~/ballistics_training:/workspace&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-w&lt;span class="w"&gt; &lt;/span&gt;/workspace&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;rocm/pytorch-nightly:latest&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;bash&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'pip install ultralytics &amp;amp;&amp;amp; python3 test_yolo.py'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The nightly container included MIOpen 3.5.1 from the develop branch.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# test_yolo.py&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;ultralytics&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;YOLO&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"PyTorch: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__version__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"CUDA available: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Device: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_device_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;YOLO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"yolov8n.pt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"data_docker.yaml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;epochs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;imgsz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;416&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cuda:0"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Result:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;✅ SUCCESS! Nightly build FIXES gfx1151 batch normalization!
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It worked! The &lt;code&gt;miopenStatusUnknownError&lt;/code&gt; was gone, no more inline assembly compilation failures. Training completed successfully with MIOpen 3.5.1 from the develop branch. The newer version had updated the batch normalization kernels to use instructions compatible with RDNA 3.5's gfx1151 architecture.&lt;/p&gt;
&lt;p&gt;This confirmed the Reddit user's tip: the fix was indeed in the newer MIOpen code that hadn't been released in a stable version yet.&lt;/p&gt;
&lt;h3&gt;The Solution: Building MIOpen from Source&lt;/h3&gt;
&lt;p&gt;Docker was great for testing, but I needed a permanent solution for my native conda environment. That meant building MIOpen 3.5.1 from source.&lt;/p&gt;
&lt;h4&gt;Step 1: Clone the Repository&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/ballistics_training
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/ROCm/MIOpen.git&lt;span class="w"&gt; &lt;/span&gt;rocm-libraries/projects/miopen
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rocm-libraries/projects/miopen
git&lt;span class="w"&gt; &lt;/span&gt;checkout&lt;span class="w"&gt; &lt;/span&gt;develop&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Latest development branch with gfx1151 fixes&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Step 2: Build MIOpen&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;build

cmake&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-DCMAKE_PREFIX_PATH&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/rocm"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-DCMAKE_INSTALL_PREFIX&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/ballistics_training/rocm-libraries/projects/miopen/build"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-DMIOPEN_BACKEND&lt;span class="o"&gt;=&lt;/span&gt;HIP&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-DCMAKE_BUILD_TYPE&lt;span class="o"&gt;=&lt;/span&gt;Release&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;..

make&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="k"&gt;$(&lt;/span&gt;nproc&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;98&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Building&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CXX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;CMakeFiles&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;MIOpen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;softmax_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cpp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Linking&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CXX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;library&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;libMIOpen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;so&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Built&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MIOpen&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Success! MIOpen 3.5.1 was built from source.&lt;/p&gt;
&lt;h4&gt;Step 3: Install Custom MIOpen to Conda Environment&lt;/h4&gt;
&lt;p&gt;Now came the tricky part: replacing the system MIOpen (version 3.0.5.1) with my custom-built version 3.5.1.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nv"&gt;CONDA_LIB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/anaconda3/envs/pt2.8-rocm7/lib

&lt;span class="c1"&gt;# Backup the original MIOpen&lt;/span&gt;
cp&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$CONDA_LIB&lt;/span&gt;/libMIOpen.so.1.0&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$CONDA_LIB&lt;/span&gt;/libMIOpen.so.1.0.backup_system

&lt;span class="c1"&gt;# Install custom MIOpen&lt;/span&gt;
cp&lt;span class="w"&gt; &lt;/span&gt;~/ballistics_training/rocm-libraries/projects/miopen/build/lib/libMIOpen.so.1.0&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$CONDA_LIB&lt;/span&gt;/

&lt;span class="c1"&gt;# Update symlinks&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$CONDA_LIB&lt;/span&gt;
ln&lt;span class="w"&gt; &lt;/span&gt;-sf&lt;span class="w"&gt; &lt;/span&gt;libMIOpen.so.1.0&lt;span class="w"&gt; &lt;/span&gt;libMIOpen.so.1
ln&lt;span class="w"&gt; &lt;/span&gt;-sf&lt;span class="w"&gt; &lt;/span&gt;libMIOpen.so.1&lt;span class="w"&gt; &lt;/span&gt;libMIOpen.so
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Step 4: Verify the Installation&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;conda&lt;span class="w"&gt; &lt;/span&gt;activate&lt;span class="w"&gt; &lt;/span&gt;pt2.8-rocm7
python&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"import torch; print(f'MIOpen version: {torch.backends.cudnn.version()}')"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;MIOpen version: 3005001
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Wait, &lt;code&gt;3005001&lt;/code&gt;? That's version 3.5.1! (MIOpen uses an integer versioning scheme: major&lt;em&gt;1000000 + minor&lt;/em&gt;1000 + patch)&lt;/p&gt;
&lt;p&gt;The custom MIOpen was successfully loaded.&lt;/p&gt;
&lt;h3&gt;The Final Test: YOLOv8 Training&lt;/h3&gt;
&lt;p&gt;Time for the moment of truth. Could I finally train YOLOv8 on my AMD GPU?&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;ultralytics&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;YOLO&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"="&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Testing YOLOv8 Training with Custom MIOpen 3.5.1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"="&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"PyTorch: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__version__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"CUDA available: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"MIOpen version: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backends&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cudnn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;YOLO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"yolov8n.pt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Starting training..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"data/bullet_hole_dataset_combined/data.yaml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;epochs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;imgsz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;416&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cuda:0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bullet_hole_detector"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;============================================================
Testing YOLOv8 Training with Custom MIOpen 3.5.1
============================================================
PyTorch: 2.8.0+rocm7.0.0.git64359f59
CUDA available: True
MIOpen version: 3005001

Starting training...

Ultralytics 8.3.217 🚀 Python-3.12.11 torch-2.8.0+rocm7.0.0 CUDA:0 (AMD Radeon Graphics, 98304MiB)

Model summary: 129 layers, 3,011,043 parameters, 3,011,027 gradients, 8.2 GFLOPs

Transferred 319/355 items from pretrained weights
AMP: running Automatic Mixed Precision (AMP) checks...
AMP: checks passed ✅

Starting training for 1 epochs...

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
        1/1     0.172G      3.022      3.775      1.215         29        416
        1/1     0.174G      2.961      4.034      1.147         46        416
        1/1     0.203G      3.133       4.08      1.251         36        416
        1/1     0.205G       3.14      4.266       1.25         60        416
        1/1     0.205G      3.028      4.194      1.237         18        416
        1/1     0.205G      2.995      4.114      1.235         28        416
        1/1     0.205G      3.029      4.118      1.226         41        416
        1/1     0.205G      2.961      4.031      1.209         26        416
        1/1     0.205G      2.888      3.998      1.193         22        416
        1/1     0.205G      2.861      3.823      1.185         49        416
        1/1     0.205G      2.812      3.657      1.169         46        416
        1/1     0.205G      2.821      3.459      1.149         78        416
        1/1     0.205G      2.776      3.253      1.134         26        416
        1/1     0.217G      2.784      3.207      1.131        122        416
        1/1     0.217G      2.772      3.074      1.121         40        416
        1/1     0.217G      2.774       2.98      1.114         13        416
        1/1     0.217G      2.763      2.914      1.118         37        416
        1/1     0.217G       2.75      2.876      1.113         81        416
        1/1     0.217G      2.731      2.799      1.104         31        416
        1/1     0.217G      2.736      2.732      1.101         30        416: 100% 14.8it/s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95)
                   all         60        733      0.653      0.473       0.53      0.191

1 epochs completed in 0.002 hours.

==============================================================
✅ SUCCESS! Training completed without errors!
==============================================================

Speed: 0.0ms preprocess, 1.9ms inference, 0.0ms loss, 0.5ms postprocess per image
Results saved to runs/detect/bullet_hole_detector/
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It worked! Batch normalization executed flawlessly. The training progressed smoothly from epoch to epoch, with GPU utilization staying high, memory management remaining stable, and losses converging as expected. The model achieved 53.0% mAP50 and trained without a single error.&lt;/p&gt;
&lt;p&gt;After a week of debugging, version wrangling, and source code compilation, I finally had GPU-accelerated YOLOv8 training working on my AMD RDNA 3.5 GPU. The custom MIOpen 3.5.1 build resolved the inline assembly compatibility issues, and training now runs as smoothly on gfx1151 as it would on any other supported GPU.&lt;/p&gt;
&lt;h3&gt;Performance Notes&lt;/h3&gt;
&lt;p&gt;With the custom MIOpen build, training performance was excellent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Training Speed: 70.5 images/second (batch size 16, 416×416 images)&lt;/li&gt;
&lt;li&gt;Training Time: 32.6 seconds for 10 epochs (2,300 total images)&lt;/li&gt;
&lt;li&gt;Throughput: 9.7-9.9 iterations/second&lt;/li&gt;
&lt;li&gt;GPU Utilization: ~95% during training with no throttling&lt;/li&gt;
&lt;li&gt;Memory Usage: ~1.2 GB VRAM for YOLOv8n with batch size 16&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The GPU utilization stayed consistently high with no performance degradation across epochs. Each epoch averaged approximately 3.3 seconds with solid consistency. For comparison, CPU-only training on the same dataset would be roughly 15-20x slower. The GPU acceleration was well worth the effort.&lt;/p&gt;
&lt;h3&gt;Lessons Learned&lt;/h3&gt;
&lt;p&gt;This debugging journey taught me several valuable lessons:&lt;/p&gt;
&lt;h4&gt;1. The ROCm Community is Invaluable&lt;/h4&gt;
&lt;p&gt;The Reddit r/ROCm community proved to be the key to solving this issue. When official documentation fails, community knowledge fills the gap. Don't hesitate to ask for help; chances are someone has encountered your exact issue before.&lt;/p&gt;
&lt;h4&gt;2. MIOpen ≠ ROCm&lt;/h4&gt;
&lt;p&gt;I initially assumed upgrading ROCm would fix the problem. In reality, MIOpen (the deep learning library) had a separate bug that was independent of the ROCm platform version. Understanding the component architecture of ROCm saved hours of debugging time.&lt;/p&gt;
&lt;h4&gt;3. RDNA 3.5 (gfx1151) Support is Still Maturing&lt;/h4&gt;
&lt;p&gt;AMD's latest integrated GPU architecture is powerful, but ML support lags behind older architectures like RDNA 2 (gfx1030) and Vega. If you're doing serious ML work on AMD, consider that newer hardware may require more troubleshooting.&lt;/p&gt;
&lt;h4&gt;4. Nightly Builds Can Be Production-Ready&lt;/h4&gt;
&lt;p&gt;There's often hesitation to use nightly/development builds in production. However, in this case, the develop branch of MIOpen was actually more stable than the official release for my specific GPU. Sometimes bleeding-edge code is exactly what you need.&lt;/p&gt;
&lt;h4&gt;5. Docker is Great for Testing&lt;/h4&gt;
&lt;p&gt;The ROCm nightly Docker containers were instrumental in proving my hypothesis. Being able to test a newer MIOpen version without committing to a full rebuild saved significant time.&lt;/p&gt;
&lt;h4&gt;6. Source Builds Give You Control&lt;/h4&gt;
&lt;p&gt;Building from source is time-consuming and requires understanding the build system, but it gives you complete control over your environment. When binary distributions fail, source builds are your safety net.&lt;/p&gt;
&lt;h3&gt;Tips for AMD GPU Machine Learning&lt;/h3&gt;
&lt;p&gt;If you're attempting to do machine learning on AMD GPUs, here are some recommendations:&lt;/p&gt;
&lt;h4&gt;Environment Setup&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Use conda/virtualenv: Isolate your Python environment to avoid system package conflicts&lt;/li&gt;
&lt;li&gt;Pin your versions: Lock PyTorch, ROCm, and MIOpen versions once you have a working setup&lt;/li&gt;
&lt;li&gt;Keep backups: Always backup working library files before swapping them out&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Debugging Strategy&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Verify GPU detection first: Ensure &lt;code&gt;torch.cuda.is_available()&lt;/code&gt; returns &lt;code&gt;True&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Test simple operations: Try basic tensor operations before complex models&lt;/li&gt;
&lt;li&gt;Check MIOpen version: &lt;code&gt;torch.backends.cudnn.version()&lt;/code&gt; can reveal version mismatches&lt;/li&gt;
&lt;li&gt;Monitor logs: ROCm logs (&lt;code&gt;MIOPEN_ENABLE_LOGGING=1&lt;/code&gt;) provide valuable debugging info&lt;/li&gt;
&lt;li&gt;Try Docker first: Test potential fixes in Docker before modifying your system&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Hardware Considerations&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;RDNA 2 (gfx1030) is more mature than RDNA 3.5 (gfx1151) for ML workloads&lt;/li&gt;
&lt;li&gt;Server GPUs (MI series) have better ROCm support than consumer cards&lt;/li&gt;
&lt;li&gt;Integrated GPUs with large shared memory (like the Radeon 8060S with 96GB) offer unique advantages for ML&lt;/li&gt;
&lt;li&gt;Check compatibility: Always verify your specific GPU (gfx code) is supported before purchasing&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Getting YOLOv8 training working on an AMD RDNA 3.5 GPU wasn't easy, but it was achievable. The combination of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Community support from r/ROCm pointing me to the right solution&lt;/li&gt;
&lt;li&gt;Docker testing to verify the fix&lt;/li&gt;
&lt;li&gt;Building MIOpen 3.5.1 from source&lt;/li&gt;
&lt;li&gt;Carefully replacing system libraries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...resulted in a fully functional GPU-accelerated machine learning training environment.&lt;/p&gt;
&lt;p&gt;AMD's ROCm platform still has rough edges compared to NVIDIA's CUDA ecosystem, but it's improving rapidly. With some patience, persistence, and willingness to dig into source code, AMD GPUs can absolutely be viable for machine learning workloads.&lt;/p&gt;
&lt;p&gt;The bullet hole detection model trained successfully, achieved excellent accuracy, and now runs in production. Sometimes the journey is as valuable as the destination; I learned more about ROCm internals, library dependencies, and GPU computing in this week than I would have in months of smooth sailing.&lt;/p&gt;
&lt;p&gt;If you're facing similar issues with AMD GPUs and ROCm, I hope this guide helps. And remember: when in doubt, check r/ROCm. The community might just have the answer you're looking for.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;System Details (for reference):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU: AMD RYZEN AI MAX+ 395&lt;/li&gt;
&lt;li&gt;GPU: AMD Radeon 8060S (integrated, gfx1151)&lt;/li&gt;
&lt;li&gt;VRAM: 96GB shared system memory&lt;/li&gt;
&lt;li&gt;ROCm: 7.0.2&lt;/li&gt;
&lt;li&gt;ROCk module: 6.14.14&lt;/li&gt;
&lt;li&gt;PyTorch: 2.8.0+rocm7.0.0.git64359f59&lt;/li&gt;
&lt;li&gt;MIOpen: 3.5.1 (custom build from develop branch)&lt;/li&gt;
&lt;li&gt;Conda Environment: pt2.8-rocm7&lt;/li&gt;
&lt;li&gt;YOLOv8: Ultralytics 8.3.217&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Key Files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MIOpen source: https://github.com/ROCm/MIOpen&lt;/li&gt;
&lt;li&gt;Ultralytics YOLOv8: https://github.com/ultralytics/ultralytics&lt;/li&gt;
&lt;li&gt;ROCm installation: https://rocm.docs.amd.com/&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Special thanks to the r/ROCm community for pointing me toward the MIOpen develop branch fix!&lt;/p&gt;</description><category>amd gpu</category><category>batch normalization</category><category>debugging</category><category>deep learning</category><category>gpu training</category><category>machine learning</category><category>miopen</category><category>object detection</category><category>pytorch</category><category>rdna 3</category><category>rocm</category><category>ultralytics</category><category>yolov8</category><guid>https://tinycomputers.io/posts/getting-yolov8-training-working-on-amd-ryzentm-al-max%2B-395.html</guid><pubDate>Wed, 22 Oct 2025 14:54:43 GMT</pubDate></item><item><title>Getting PyTorch Working with AMD Radeon Pro W7900 (MAX+ 395): A Comprehensive Guide</title><link>https://tinycomputers.io/posts/getting-pytorch-working-with-amd-radeon-pro-w7900-max%2B-395-a-comprehensive-guide.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;p&gt;&lt;audio controls&gt;
  &lt;source src="https://tinycomputers.io/getting-pytorch-working-with-amd-radeon-pro-w7900-max+-395-a-comprehensive-guide_tts.mp3" type="audio/mpeg"&gt;
  Your browser does not support the audio element.
&lt;/source&gt;&lt;/audio&gt;&lt;/p&gt;
&lt;h2&gt;Getting PyTorch Working with AMD Radeon Pro W7900 (MAX+ 395): A Comprehensive Guide&lt;/h2&gt;
&lt;h3&gt;Introduction&lt;/h3&gt;
&lt;p&gt;The AMD Radeon Pro W7900 represents a significant leap forward in professional GPU computing. With 96GB of unified memory and 20 compute units, this workstation-class GPU brings serious computational power to tasks like machine learning, scientific computing, and data analysis. However, getting deep learning frameworks like PyTorch to work with AMD GPUs has historically been more challenging than with NVIDIA's CUDA ecosystem.&lt;/p&gt;
&lt;p&gt;Here's a complete walkthrough of setting up PyTorch with ROCm support on the AMD MAX+ 395, including installation, verification, and real-world testing. By the end, you'll have a fully functional PyTorch environment capable of leveraging your AMD GPU's computational power.&lt;/p&gt;
&lt;h3&gt;Understanding ROCm and PyTorch&lt;/h3&gt;
&lt;h4&gt;What is ROCm?&lt;/h4&gt;
&lt;p&gt;ROCm (Radeon Open Compute) is AMD's open-source software platform for GPU computing. It serves as AMD's answer to NVIDIA's CUDA, providing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Low-level GPU programming interfaces&lt;/li&gt;
&lt;li&gt;Optimized libraries for linear algebra, FFT, and other operations&lt;/li&gt;
&lt;li&gt;Deep learning framework support&lt;/li&gt;
&lt;li&gt;Compatibility with CUDA-based code through HIP (Heterogeneous-compute Interface for Portability)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;PyTorch and ROCm Integration&lt;/h4&gt;
&lt;p&gt;PyTorch has officially supported ROCm since version 1.8, and support has matured significantly over subsequent releases. The ROCm version of PyTorch uses the same API as the CUDA version, making it straightforward to port existing PyTorch code to AMD GPUs. In fact, most PyTorch code written for CUDA will work without modification on ROCm, as the framework abstracts away the underlying GPU platform.&lt;/p&gt;
&lt;h3&gt;System Specifications&lt;/h3&gt;
&lt;p&gt;Testing was performed on a system with the following specifications:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPU&lt;/strong&gt;: AMD Radeon Pro W7900 (MAX+ 395)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU Memory&lt;/strong&gt;: 96 GB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute Units&lt;/strong&gt;: 20&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CUDA Capability&lt;/strong&gt;: 11.5 (ROCm compatibility level)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operating System&lt;/strong&gt;: Linux&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python&lt;/strong&gt;: 3.12.11&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PyTorch Version&lt;/strong&gt;: 2.8.0+rocm7.0.0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROCm Version&lt;/strong&gt;: 7.0.0&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Installation and Setup&lt;/h3&gt;
&lt;p&gt;This section provides detailed, step-by-step instructions for bootstrapping a complete ROCm 7.0 + PyTorch 2.8 environment on Ubuntu 24.04.3 LTS. These instructions are based on successful installations on the AMD Ryzen AI Max+395 platform.&lt;/p&gt;
&lt;h4&gt;Prerequisites&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu 24.04.3 LTS (Server or Desktop)&lt;/li&gt;
&lt;li&gt;Administrator/sudo access&lt;/li&gt;
&lt;li&gt;Internet connection for downloading packages&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Step 1: Update Linux Kernel&lt;/h4&gt;
&lt;p&gt;ROCm 7.0 works best with Linux kernel 6.14 or later. Update your kernel:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;linux-generic-hwe-24.04
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Verify the installation:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;cat&lt;span class="w"&gt; &lt;/span&gt;/proc/version
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You should see output similar to:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;Linux&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;6.14.0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;generic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buildd&lt;/span&gt;&lt;span class="nv"&gt;@lcy02&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;amd64&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;026&lt;/span&gt;&lt;span class="p"&gt;)...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Reboot to load the new kernel:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;reboot
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Step 2: Install AMDGPU Driver&lt;/h4&gt;
&lt;p&gt;First, set up the AMD repository:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Create keyring directory if it doesn't exist&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;--parents&lt;span class="w"&gt; &lt;/span&gt;--mode&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0755&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/etc/apt/keyrings

&lt;span class="c1"&gt;# Download and install AMD GPG key&lt;/span&gt;
wget&lt;span class="w"&gt; &lt;/span&gt;https://repo.radeon.com/rocm/rocm.gpg.key&lt;span class="w"&gt; &lt;/span&gt;-O&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;gpg&lt;span class="w"&gt; &lt;/span&gt;--dearmor&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;/etc/apt/keyrings/rocm.gpg&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/dev/null

&lt;span class="c1"&gt;# Add AMDGPU repository&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;/etc/apt/sources.list.d/amdgpu.list&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt; EOF&lt;/span&gt;
&lt;span class="s"&gt;deb [arch=amd64,i386 signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/amdgpu/latest/ubuntu noble main&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Install the AMDGPU DKMS driver:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;update
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;amdgpu-dkms
sudo&lt;span class="w"&gt; &lt;/span&gt;reboot
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Verify the driver installation:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;dkms&lt;span class="w"&gt; &lt;/span&gt;status
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You should see output like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;amdgpu/6.14.14-2212064.24.04, 6.14.0-33-generic, x86_64: installed
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Step 3: Install ROCm 7.0&lt;/h4&gt;
&lt;p&gt;Install prerequisites:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;python3-setuptools&lt;span class="w"&gt; &lt;/span&gt;python3-wheel
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;update
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Download and install the AMD GPU installer:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;wget&lt;span class="w"&gt; &lt;/span&gt;https://repo.radeon.com/amdgpu-install/7.0/ubuntu/noble/amdgpu-install_7.0.70000-1_all.deb
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;./amdgpu-install_7.0.70000-1_all.deb
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Install ROCm with the compute use case (choose Y when prompted to overwrite amdgpu.list):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;amdgpu-install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;--usecase&lt;span class="o"&gt;=&lt;/span&gt;rocm
sudo&lt;span class="w"&gt; &lt;/span&gt;reboot
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Add your user to the required groups:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;usermod&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;-G&lt;span class="w"&gt; &lt;/span&gt;render,video&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$LOGNAME&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;reboot
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Verify ROCm installation:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;rocminfo
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You should see your GPU listed as an agent with detailed properties.&lt;/p&gt;
&lt;h4&gt;Step 4: Configure ROCm Libraries&lt;/h4&gt;
&lt;p&gt;Configure the system to find ROCm shared libraries:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Add ROCm library paths&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;--append&lt;span class="w"&gt; &lt;/span&gt;/etc/ld.so.conf.d/rocm.conf&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;EOF&lt;/span&gt;
&lt;span class="s"&gt;/opt/rocm/lib&lt;/span&gt;
&lt;span class="s"&gt;/opt/rocm/lib64&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;

sudo&lt;span class="w"&gt; &lt;/span&gt;ldconfig

&lt;span class="c1"&gt;# Set library path environment variable (add to ~/.bashrc for persistence)&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;LD_LIBRARY_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/rocm-7.0.0/lib:&lt;span class="nv"&gt;$LD_LIBRARY_PATH&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Install and verify OpenCL runtime:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;rocm-opencl-runtime
clinfo
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;clinfo&lt;/code&gt; command should display information about your AMD GPU.&lt;/p&gt;
&lt;h4&gt;Step 5: Install PyTorch with ROCm Support&lt;/h4&gt;
&lt;p&gt;Create a conda environment and install PyTorch:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Create conda environment&lt;/span&gt;
conda&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;pt2.8-rocm7&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;python&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;.12
conda&lt;span class="w"&gt; &lt;/span&gt;activate&lt;span class="w"&gt; &lt;/span&gt;pt2.8-rocm7

&lt;span class="c1"&gt;# Install PyTorch 2.8.0 with ROCm 7.0 from AMD's repository&lt;/span&gt;
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0/pytorch_triton_rocm-3.2.0%2Brocm7.0.0.4d510c3a44-cp312-cp312-linux_x86_64.whl
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0/torch-2.8.0%2Brocm7.0.0-cp312-cp312-linux_x86_64.whl
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0/torchvision-0.23.0%2Brocm7.0.0-cp312-cp312-linux_x86_64.whl
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0/torchaudio-2.8.0%2Brocm7.0.0-cp312-cp312-linux_x86_64.whl

&lt;span class="c1"&gt;# Install GCC 12.1 (required for some operations)&lt;/span&gt;
conda&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;conda-forge&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;gcc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;.1.0
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Important Notes&lt;/strong&gt;:
- The URLs above are for Python 3.12 (cp312). Adjust for your Python version if different.
- These wheels are built specifically for ROCm 7.0 and may not work with other ROCm versions.
- The &lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt; must be set correctly, or PyTorch won't find ROCm libraries.&lt;/p&gt;
&lt;h4&gt;Verifying Installation&lt;/h4&gt;
&lt;p&gt;After installation, perform a quick verification:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"PyTorch version: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__version__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"CUDA available: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Device count: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device_count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Device name: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_device_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that despite using ROCm, PyTorch still refers to the GPU API as "CUDA" for compatibility reasons. This is intentional and allows CUDA-based code to run on AMD GPUs without modification.&lt;/p&gt;
&lt;h3&gt;Comprehensive GPU Testing&lt;/h3&gt;
&lt;p&gt;To thoroughly validate that PyTorch is working correctly with the MAX+ 395, we developed a comprehensive test suite that exercises various aspects of GPU computing.&lt;/p&gt;
&lt;h4&gt;Test Suite Overview&lt;/h4&gt;
&lt;p&gt;Our test suite includes five major components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Installation Verification&lt;/strong&gt;: Confirms PyTorch version and GPU detection&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROCm Availability Check&lt;/strong&gt;: Validates GPU properties and capabilities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tensor Operations&lt;/strong&gt;: Tests basic tensor creation and mathematical operations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Neural Network Operations&lt;/strong&gt;: Validates deep learning functionality&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Management&lt;/strong&gt;: Tests GPU memory allocation and deallocation&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Test Script&lt;/h4&gt;
&lt;p&gt;Here's the complete test script we developed:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="ch"&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;span class="sd"&gt;"""&lt;/span&gt;
&lt;span class="sd"&gt;ROCm PyTorch GPU Test POC&lt;/span&gt;
&lt;span class="sd"&gt;Tests if ROCm PyTorch can successfully detect and use AMD GPUs&lt;/span&gt;
&lt;span class="sd"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;print_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Print a formatted section header"""&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;" &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_pytorch_installation&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Test basic PyTorch installation"""&lt;/span&gt;
    &lt;span class="n"&gt;print_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"PyTorch Installation Info"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"PyTorch Version: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__version__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Python Version: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_rocm_availability&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Test ROCm/CUDA availability"""&lt;/span&gt;
    &lt;span class="n"&gt;print_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ROCm/CUDA Availability"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;cuda_available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"CUDA Available: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cuda_available&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cuda_available&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"CUDA Device Count: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device_count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Current Device: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_device&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Device Name: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_device_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_device_properties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Device Properties:"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"  - Total Memory: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_memory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; GB"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"  - Multi Processor Count: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;multi_processor_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"  - CUDA Capability: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;major&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minor&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"No CUDA/ROCm devices detected!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_tensor_operations&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Test basic tensor operations on GPU"""&lt;/span&gt;
    &lt;span class="n"&gt;print_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Tensor Operations Test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cpu_tensor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"CPU Tensor created: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cpu_tensor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"CPU Tensor device: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cpu_tensor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;gpu_tensor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cpu_tensor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;GPU Tensor created: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;gpu_tensor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"GPU Tensor device: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;gpu_tensor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Performing matrix multiplication on GPU..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matmul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpu_tensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gpu_tensor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Result shape: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Result device: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;cpu_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Moved result back to CPU: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cpu_result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;✓ Tensor operations successful!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;✗ Tensor operations failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_simple_neural_network&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Test a simple neural network operation on GPU"""&lt;/span&gt;
    &lt;span class="n"&gt;print_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Neural Network Test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReLU&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Model created on CPU"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Model device: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Model moved to GPU: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;input_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Input data shape: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Input data device: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Performing forward pass..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Output shape: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Output device: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;✓ Neural network test successful!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;✗ Neural network test failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_memory_management&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Test GPU memory management"""&lt;/span&gt;
    &lt;span class="n"&gt;print_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"GPU Memory Management Test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Allocated Memory: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_allocated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; MB"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Cached Memory: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_reserved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; MB"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;tensors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;tensors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;After allocating 5 tensors:"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Allocated Memory: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_allocated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; MB"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Cached Memory: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_reserved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; MB"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;tensors&lt;/span&gt;
            &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty_cache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;After clearing cache:"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Allocated Memory: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_allocated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; MB"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Cached Memory: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_reserved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; MB"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;✓ Memory management test successful!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"No GPU available for memory test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;✗ Memory management test failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Run all tests"""&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"="&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" ROCm PyTorch GPU Test POC"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"="&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;test_pytorch_installation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;test_rocm_availability&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"="&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" FAILED: No ROCm/CUDA devices available"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"="&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s2"&gt;"Tensor Operations"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_tensor_operations&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s2"&gt;"Neural Network"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_simple_neural_network&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s2"&gt;"Memory Management"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_memory_management&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;

    &lt;span class="n"&gt;print_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Test Summary"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;all_passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;test_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"✓ PASSED"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;"✗ FAILED"&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;test_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;all_passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"="&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;all_passed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" SUCCESS: All tests passed! ROCm GPU is working."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" PARTIAL SUCCESS: Some tests failed."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"="&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;all_passed&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Test Results and Analysis&lt;/h3&gt;
&lt;p&gt;Running our comprehensive test suite on the MAX+ 395 yielded excellent results across all categories.&lt;/p&gt;
&lt;h4&gt;GPU Detection and Properties&lt;/h4&gt;
&lt;p&gt;The first test confirmed that PyTorch successfully detected the AMD GPU:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;CUDA Available: True
CUDA Device Count: 1
Current Device: 0
Device Name: AMD Radeon Graphics

Device Properties:
  - Total Memory: 96.00 GB
  - Multi Processor Count: 20
  - CUDA Capability: 11.5
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The 96GB of memory is particularly impressive, far exceeding what's available on most consumer or even professional NVIDIA GPUs. This massive memory capacity opens up possibilities for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Training larger models without splitting across multiple GPUs&lt;/li&gt;
&lt;li&gt;Processing high-resolution images or long sequences&lt;/li&gt;
&lt;li&gt;Handling larger batch sizes for improved training efficiency&lt;/li&gt;
&lt;li&gt;Running multiple models simultaneously&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Tensor Operations Performance&lt;/h4&gt;
&lt;p&gt;Basic tensor operations executed flawlessly:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;CPU Tensor created: torch.Size([1000, 1000])
CPU Tensor device: cpu

GPU Tensor created: torch.Size([1000, 1000])
GPU Tensor device: cuda:0

Performing matrix multiplication on GPU...
Result shape: torch.Size([1000, 1000])
Result device: cuda:0
Moved result back to CPU: cpu

✓ Tensor operations successful!
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The seamless movement of tensors between CPU and GPU memory, along with successful matrix multiplication, confirms that the fundamental PyTorch operations work correctly on ROCm.&lt;/p&gt;
&lt;h4&gt;Neural Network Operations&lt;/h4&gt;
&lt;p&gt;Our neural network test validated that PyTorch's high-level APIs work correctly:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Model created on CPU
Model device: cpu
Model moved to GPU: cuda:0

Input data shape: torch.Size([32, 100])
Input data device: cuda:0
Performing forward pass...
Output shape: torch.Size([32, 10])
Output device: cuda:0

✓ Neural network test successful!
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This test confirms that:
- Models can be moved to GPU with the &lt;code&gt;.cuda()&lt;/code&gt; method
- Forward passes execute correctly on GPU
- All layers (Linear, ReLU) are properly accelerated&lt;/p&gt;
&lt;h4&gt;Memory Management&lt;/h4&gt;
&lt;p&gt;The memory management test showed efficient allocation and deallocation:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Allocated Memory: 32.00 MB
Cached Memory: 54.00 MB

After allocating 5 tensors:
Allocated Memory: 52.00 MB
Cached Memory: 54.00 MB

After clearing cache:
Allocated Memory: 32.00 MB
Cached Memory: 32.00 MB
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;PyTorch's memory management on ROCm works identically to CUDA, with proper caching behavior and the ability to manually clear cached memory when needed.&lt;/p&gt;
&lt;h3&gt;Performance Considerations&lt;/h3&gt;
&lt;h4&gt;Memory Bandwidth&lt;/h4&gt;
&lt;p&gt;The MAX+ 395's 96GB of memory is a significant advantage, but memory bandwidth is equally important for deep learning workloads. The W7900's memory subsystem provides substantial bandwidth for data transfers between GPU memory and compute units.&lt;/p&gt;
&lt;h4&gt;Compute Performance&lt;/h4&gt;
&lt;p&gt;With 20 compute units, the MAX+ 395 provides substantial parallel processing capability. While direct comparisons to NVIDIA GPUs depend on the specific workload, ROCm's optimization for AMD architectures ensures efficient utilization of available compute resources.&lt;/p&gt;
&lt;h4&gt;Software Maturity&lt;/h4&gt;
&lt;p&gt;ROCm has matured significantly over recent years. Most PyTorch operations that work on CUDA now work seamlessly on ROCm. However, some edge cases and newer features may still have better support on CUDA, so testing your specific workload is recommended.&lt;/p&gt;
&lt;h3&gt;Practical Tips and Best Practices&lt;/h3&gt;
&lt;h4&gt;Code Portability&lt;/h4&gt;
&lt;p&gt;To write code that works on both CUDA and ROCm:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Use device-agnostic code&lt;/span&gt;
&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cuda"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Monitoring GPU Utilization&lt;/h4&gt;
&lt;p&gt;Use &lt;code&gt;rocm-smi&lt;/code&gt; to monitor GPU utilization:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;watch&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rocm-smi
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This provides real-time information about GPU usage, memory consumption, temperature, and power draw.&lt;/p&gt;
&lt;h4&gt;Optimizing Memory Usage&lt;/h4&gt;
&lt;p&gt;With 96GB available, you might be tempted to use very large batch sizes. However, optimal batch size depends on many factors:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Experiment with batch sizes&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# Train and measure throughput&lt;/span&gt;
    &lt;span class="c1"&gt;# Find the sweet spot between memory usage and performance&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Debugging&lt;/h4&gt;
&lt;p&gt;Enable PyTorch's anomaly detection during development:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;autograd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_detect_anomaly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Troubleshooting Common Issues&lt;/h3&gt;
&lt;h4&gt;GPU Not Detected&lt;/h4&gt;
&lt;p&gt;If &lt;code&gt;torch.cuda.is_available()&lt;/code&gt; returns &lt;code&gt;False&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Verify ROCm installation: &lt;code&gt;rocm-smi&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Check PyTorch was installed with ROCm support: &lt;code&gt;print(torch.__version__)&lt;/code&gt; should show &lt;code&gt;+rocm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Ensure ROCm drivers match PyTorch's ROCm version&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Out of Memory Errors&lt;/h4&gt;
&lt;p&gt;Even with 96GB, you can run out of memory:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Clear cache periodically&lt;/span&gt;
&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty_cache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Use gradient checkpointing for large models&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch.utils.checkpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;checkpoint&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Performance Issues&lt;/h4&gt;
&lt;p&gt;If training is slower than expected:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Profile your code: &lt;code&gt;torch.profiler.profile()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Check for CPU-GPU transfer bottlenecks&lt;/li&gt;
&lt;li&gt;Verify data loading isn't the bottleneck&lt;/li&gt;
&lt;li&gt;Consider using mixed precision training with &lt;code&gt;torch.cuda.amp&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The AMD Radeon Pro W7900 (MAX+ 395) with ROCm provides a robust, capable platform for PyTorch-based machine learning workloads. Our comprehensive testing demonstrated that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PyTorch 2.8.0 with ROCm 7.0.0 works seamlessly with the MAX+ 395&lt;/li&gt;
&lt;li&gt;All tested operations (tensors, neural networks, memory management) function correctly&lt;/li&gt;
&lt;li&gt;The massive 96GB memory capacity enables unique use cases&lt;/li&gt;
&lt;li&gt;Code written for CUDA generally works without modification&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For organizations invested in AMD hardware or looking for alternatives to NVIDIA's ecosystem, the MAX+ 395 with ROCm represents a viable option for deep learning workloads. The open-source nature of ROCm and PyTorch's strong support for the platform ensure that AMD GPUs are first-class citizens in the deep learning community.&lt;/p&gt;
&lt;p&gt;As ROCm continues to evolve and PyTorch support deepens, AMD's GPU offerings will only become more compelling for machine learning practitioners. The MAX+ 395, with its exceptional memory capacity and solid compute performance, stands ready to tackle demanding deep learning tasks.&lt;/p&gt;
&lt;h3&gt;Acknowledgments&lt;/h3&gt;
&lt;p&gt;The detailed ROCm 7.0 installation procedure is based on Wei Lu's excellent article "&lt;a href="https://baud.rs/64est6"&gt;Ultralytics YOLO/SAM with ROCm 7.0 on AMD Ryzen AI Max+395 'Strix Halo'&lt;/a&gt;" published on Medium in October 2025. Wei Lu's pioneering work in documenting the complete bootstrapping process for ROCm 7.0 on the Max+395 platform made this possible.&lt;/p&gt;
&lt;h3&gt;Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/uHclTm"&gt;PyTorch ROCm Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/Ze4BjI"&gt;ROCm Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/HU9Det"&gt;AMD GPUs for Deep Learning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/B3R5RB"&gt;AMD ROCm Installation Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/64est6"&gt;Wei Lu's Original Article&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Based on real-world testing performed on October 10, 2025, using PyTorch 2.8.0 with ROCm 7.0.0 on an AMD Radeon Pro W7900 GPU with 96GB memory. Installation instructions adapted from Wei Lu's documentation of the AMD Ryzen AI Max+395 platform.&lt;/em&gt;&lt;/p&gt;</description><category>amd gpu</category><category>deep learning</category><category>gpu computing</category><category>installation guide</category><category>machine learning</category><category>pytorch</category><category>rocm</category><guid>https://tinycomputers.io/posts/getting-pytorch-working-with-amd-radeon-pro-w7900-max%2B-395-a-comprehensive-guide.html</guid><pubDate>Sat, 11 Oct 2025 23:08:14 GMT</pubDate></item><item><title>AMD AI Max+ 395 System Review: A Comprehensive Analysis</title><link>https://tinycomputers.io/posts/amd-ai-max%2B-395-system-review-a-comprehensive-analysis.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/amd-ai-max+-395-system-review-a-comprehensive-analysis_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;29 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;Executive Summary&lt;/h3&gt;
&lt;p&gt;The AMD AI Max+ 395 system represents AMD's latest entry into the high-performance computing and AI acceleration market, featuring the company's cutting-edge Strix Halo architecture. This comprehensive review examines the system's performance characteristics, software compatibility, and overall viability for AI workloads and general computing tasks. While the hardware shows impressive potential with its 16-core CPU and integrated Radeon 8060S graphics, significant software ecosystem challenges, particularly with PyTorch/ROCm compatibility for the gfx1151 architecture, present substantial barriers to immediate adoption for AI development workflows.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/IMG_3733.jpg" alt="AMD AI Max+ 395 Bosgame" style="float: left; width: 40%; margin: 0 20px 20px 0;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: An Orange Pi 5 Max was photobombing this photograph&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;System Specifications and Architecture Overview&lt;/h3&gt;
&lt;h4&gt;CPU Specifications&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Processor&lt;/strong&gt;: AMD RYZEN AI MAX+ 395 w/ Radeon 8060S&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Architecture&lt;/strong&gt;: x86_64 with Zen 5 cores&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cores/Threads&lt;/strong&gt;: 16 cores / 32 threads&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base Clock&lt;/strong&gt;: 599 MHz (minimum)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Boost Clock&lt;/strong&gt;: 5,185 MHz (maximum)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache Configuration&lt;/strong&gt;:&lt;/li&gt;
&lt;li&gt;L1d Cache: 768 KiB (16 instances, 48 KiB per core)&lt;/li&gt;
&lt;li&gt;L1i Cache: 512 KiB (16 instances, 32 KiB per core)&lt;/li&gt;
&lt;li&gt;L2 Cache: 16 MiB (16 instances, 1 MiB per core)&lt;/li&gt;
&lt;li&gt;L3 Cache: 64 MiB (2 instances, 32 MiB per CCX)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instruction Set Extensions&lt;/strong&gt;: Full AVX-512, AVX-VNNI, BF16 support&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Memory Subsystem&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Total System Memory&lt;/strong&gt;: 32 GB DDR5&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Configuration&lt;/strong&gt;: Unified memory architecture with shared GPU/CPU access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Bandwidth&lt;/strong&gt;: Achieved ~13.5 GB/s in multi-threaded tests&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Graphics Processing Unit&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPU Architecture&lt;/strong&gt;: Strix Halo (RDNA 3.5 based)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU Designation&lt;/strong&gt;: gfx1151&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute Units&lt;/strong&gt;: 40 CUs (80 reported in ROCm, likely accounting for dual SIMD per CU)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Peak GPU Clock&lt;/strong&gt;: 2,900 MHz&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VRAM&lt;/strong&gt;: 96 GB shared system memory (103 GB total addressable) - &lt;em&gt;Note: This allocation was intentionally configured to maximize GPU memory for large language model inference&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Bandwidth&lt;/strong&gt;: Shared with system memory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenCL Compute Units&lt;/strong&gt;: 20 (as reported by clinfo)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Platform Details&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Operating System&lt;/strong&gt;: Ubuntu 24.04.3 LTS (Noble)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kernel Version&lt;/strong&gt;: 6.8.0-83-generic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Architecture&lt;/strong&gt;: x86_64&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Virtualization&lt;/strong&gt;: AMD-V enabled&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Performance Benchmarks&lt;/h3&gt;
&lt;p&gt;&lt;img alt="AMD AI Max+ 395 System Analysis Dashboard" src="https://tinycomputers.io/images/amd_system_analysis.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 1: Comprehensive performance analysis and compatibility overview of the AMD AI Max+ 395 system&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;CPU Performance Analysis&lt;/h4&gt;
&lt;h5&gt;Single-Threaded Performance&lt;/h5&gt;
&lt;p&gt;The sysbench CPU benchmark with prime number calculation revealed strong single-threaded performance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Events per second&lt;/strong&gt;: 6,368.92&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Average latency&lt;/strong&gt;: 0.16 ms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;95th percentile latency&lt;/strong&gt;: 0.16 ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This performance places the AMD AI Max+ 395 in the upper tier of modern processors for single-threaded workloads, demonstrating the effectiveness of the Zen 5 architecture's IPC improvements and high boost clocks.&lt;/p&gt;
&lt;h5&gt;Multi-Threaded Performance&lt;/h5&gt;
&lt;p&gt;Multi-threaded testing across all 32 threads showed excellent scaling:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Events per second&lt;/strong&gt;: 103,690.35&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scaling efficiency&lt;/strong&gt;: 16.3x improvement over single-threaded (theoretical maximum 32x)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thread fairness&lt;/strong&gt;: Excellent distribution with minimal standard deviation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The scaling efficiency of approximately 51% indicates good multi-threading performance, though there's room for optimization in workloads that can fully utilize all available threads.&lt;/p&gt;
&lt;h4&gt;Memory Performance&lt;/h4&gt;
&lt;h5&gt;Memory Bandwidth Testing&lt;/h5&gt;
&lt;p&gt;Memory performance testing using sysbench revealed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single-threaded bandwidth&lt;/strong&gt;: 9.3 GB/s&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-threaded bandwidth&lt;/strong&gt;: 13.5 GB/s (16 threads)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency characteristics&lt;/strong&gt;: Sub-millisecond access times&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The memory bandwidth results suggest the system is well-balanced for most workloads, though AI applications requiring extremely high memory bandwidth may find this a limiting factor compared to discrete GPU solutions with dedicated VRAM.&lt;/p&gt;
&lt;h4&gt;GPU Performance and Capabilities&lt;/h4&gt;
&lt;h5&gt;Hardware Specifications&lt;/h5&gt;
&lt;p&gt;The integrated Radeon 8060S GPU presents impressive specifications on paper:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Architecture&lt;/strong&gt;: RDNA 3.5 (Strix Halo)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute Units&lt;/strong&gt;: 40 CUs with 2 SIMDs each&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Access&lt;/strong&gt;: Full 96 GB of shared system memory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clock Speed&lt;/strong&gt;: Up to 2.9 GHz&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;OpenCL Capabilities&lt;/h5&gt;
&lt;p&gt;OpenCL enumeration reveals solid compute capabilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Device Type&lt;/strong&gt;: GPU with full OpenCL 2.1 support&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Max Compute Units&lt;/strong&gt;: 20 (OpenCL reporting)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Max Work Group Size&lt;/strong&gt;: 256&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Image Support&lt;/strong&gt;: Full 2D/3D image processing capabilities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Allocation&lt;/strong&gt;: Up to 87 GB maximum allocation&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Network Performance Testing&lt;/h4&gt;
&lt;p&gt;Network infrastructure testing using iperf3 demonstrated excellent localhost performance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Loopback Bandwidth&lt;/strong&gt;: 122 Gbits/sec sustained&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency&lt;/strong&gt;: Minimal retransmissions (0 retries)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistency&lt;/strong&gt;: Stable performance across 10-second test duration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This indicates robust internal networking capabilities suitable for distributed computing scenarios and high-bandwidth data transfer requirements.&lt;/p&gt;
&lt;h3&gt;PyTorch/ROCm Compatibility Analysis&lt;/h3&gt;
&lt;h4&gt;Current State of ROCm Support&lt;/h4&gt;
&lt;p&gt;We installed ROCm 7.0 and related components:
- &lt;strong&gt;ROCm Version&lt;/strong&gt;: 7.0.0
- &lt;strong&gt;HIP Version&lt;/strong&gt;: 7.0.51831
- &lt;strong&gt;PyTorch Version&lt;/strong&gt;: 2.5.1+rocm6.2&lt;/p&gt;
&lt;h4&gt;gfx1151 Compatibility Issues&lt;/h4&gt;
&lt;p&gt;The most significant finding of this review centers on the gfx1151 architecture compatibility with current AI software stacks. Testing revealed critical limitations:&lt;/p&gt;
&lt;h5&gt;PyTorch Compatibility Problems&lt;/h5&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;rocBLAS error: Cannot read TensileLibrary.dat: Illegal seek for GPU arch : gfx1151
List of available TensileLibrary Files:
&lt;span class="k"&gt;-&lt;/span&gt; TensileLibrary_lazy_gfx1030.dat
&lt;span class="k"&gt;-&lt;/span&gt; TensileLibrary_lazy_gfx906.dat
&lt;span class="k"&gt;-&lt;/span&gt; TensileLibrary_lazy_gfx908.dat
&lt;span class="k"&gt;-&lt;/span&gt; TensileLibrary_lazy_gfx942.dat
&lt;span class="k"&gt;-&lt;/span&gt; TensileLibrary_lazy_gfx900.dat
&lt;span class="k"&gt;-&lt;/span&gt; TensileLibrary_lazy_gfx90a.dat
&lt;span class="k"&gt;-&lt;/span&gt; TensileLibrary_lazy_gfx1100.dat
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This error indicates that PyTorch's ROCm backend lacks pre-compiled optimized kernels for the gfx1151 architecture. The absence of gfx1151 in the TensileLibrary files means:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No Optimized BLAS Operations&lt;/strong&gt;: Matrix multiplication, convolutions, and other fundamental AI operations cannot leverage GPU acceleration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Training Workflows Broken&lt;/strong&gt;: Most deep learning training pipelines will fail or fall back to CPU execution&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inference Limitations&lt;/strong&gt;: Even basic neural network inference is compromised&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Root Cause Analysis&lt;/h5&gt;
&lt;p&gt;The gfx1151 architecture represents a newer GPU design that hasn't been fully integrated into the ROCm software stack. While the hardware is detected and basic OpenCL operations function, the optimized compute libraries essential for AI workloads are missing.&lt;/p&gt;
&lt;h5&gt;Workaround Attempts&lt;/h5&gt;
&lt;p&gt;Testing various workarounds yielded limited success:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HSA_OVERRIDE_GFX_VERSION=11.0.0&lt;/strong&gt;: Failed to resolve compatibility issues&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU Fallback&lt;/strong&gt;: PyTorch operates normally on CPU, but defeats the purpose of GPU acceleration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Basic GPU Operations&lt;/strong&gt;: Simple tensor allocation succeeds, but compute operations fail&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Software Ecosystem Gaps&lt;/h4&gt;
&lt;p&gt;Beyond PyTorch, the gfx1151 compatibility issues extend to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TensorFlow&lt;/strong&gt;: Likely similar rocBLAS dependency issues&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JAX&lt;/strong&gt;: ROCm backend compatibility uncertain&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scientific Computing&lt;/strong&gt;: NumPy/SciPy GPU acceleration unavailable&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Machine Learning Frameworks&lt;/strong&gt;: Most frameworks dependent on rocBLAS will encounter issues&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;AMD GPU Software Support Ecosystem Analysis&lt;/h3&gt;
&lt;h4&gt;Current State Assessment&lt;/h4&gt;
&lt;p&gt;AMD's GPU software ecosystem has made significant strides but remains fragmented compared to NVIDIA's CUDA platform:&lt;/p&gt;
&lt;h5&gt;Strengths&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Open Source Foundation&lt;/strong&gt;: ROCm's open-source nature enables community contributions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standard API Support&lt;/strong&gt;: OpenCL 2.1 and HIP provide industry-standard interfaces&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux Integration&lt;/strong&gt;: Strong kernel-level support through AMDGPU drivers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Professional Tools&lt;/strong&gt;: rocm-smi and related utilities provide comprehensive monitoring&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Weaknesses&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fragmented Architecture Support&lt;/strong&gt;: New architectures like gfx1151 lag behind in software support&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited Documentation&lt;/strong&gt;: Less comprehensive than CUDA documentation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smaller Developer Community&lt;/strong&gt;: Fewer third-party tools and optimizations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compatibility Matrix Complexity&lt;/strong&gt;: Different software versions support different GPU architectures&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Long-term Viability Concerns&lt;/h4&gt;
&lt;p&gt;The gfx1151 compatibility issues highlight broader ecosystem challenges:&lt;/p&gt;
&lt;h5&gt;Release Coordination Problems&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Hardware releases outpace software ecosystem updates&lt;/li&gt;
&lt;li&gt;Critical libraries (rocBLAS, Tensile) require architecture-specific optimization&lt;/li&gt;
&lt;li&gt;Coordination between AMD hardware and software teams appears insufficient&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Market Adoption Barriers&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Developers hesitant to adopt platform with uncertain software support&lt;/li&gt;
&lt;li&gt;Enterprise customers require guaranteed compatibility&lt;/li&gt;
&lt;li&gt;Academic researchers need stable, well-documented platforms&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Recommendations for AMD&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Accelerated Software Development&lt;/strong&gt;: Prioritize gfx1151 support in rocBLAS and related libraries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pre-release Testing&lt;/strong&gt;: Ensure software ecosystem readiness before hardware launches&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better Documentation&lt;/strong&gt;: Comprehensive compatibility matrices and migration guides&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Community Engagement&lt;/strong&gt;: More responsive developer relations and support channels&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Network Infrastructure and Connectivity&lt;/h3&gt;
&lt;p&gt;The system demonstrates excellent network performance characteristics suitable for modern computing workloads:&lt;/p&gt;
&lt;h4&gt;Internal Performance&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Memory-to-Network Efficiency&lt;/strong&gt;: 122 Gbps loopback performance indicates minimal bottlenecks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System Integration&lt;/strong&gt;: Unified memory architecture benefits network-intensive applications&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: Architecture suitable for distributed computing scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;External Connectivity Assessment&lt;/h4&gt;
&lt;p&gt;While specific external network testing wasn't performed, the system's infrastructure suggests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Support for high-speed Ethernet (2.5GbE+)&lt;/li&gt;
&lt;li&gt;Low-latency interconnects suitable for cluster computing&lt;/li&gt;
&lt;li&gt;Adequate bandwidth for data center deployment scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Power Efficiency and Thermal Characteristics&lt;/h3&gt;
&lt;p&gt;Limited thermal data was available during testing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Idle Temperature&lt;/strong&gt;: 29°C (GPU sensor)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Idle Power&lt;/strong&gt;: 8.059W (GPU subsystem)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thermal Management&lt;/strong&gt;: Appears well-controlled under light loads&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The unified architecture's power efficiency represents a significant advantage over discrete GPU solutions, particularly for mobile and edge computing applications.&lt;/p&gt;
&lt;h3&gt;Competitive Analysis&lt;/h3&gt;
&lt;h4&gt;Comparison with Intel Arc&lt;/h4&gt;
&lt;p&gt;Intel's Arc GPUs face similar software ecosystem challenges, though Intel has made more aggressive investments in AI software stack development. The Arc series benefits from Intel's deeper software engineering resources but still lags behind NVIDIA in AI framework support.&lt;/p&gt;
&lt;h4&gt;Comparison with NVIDIA&lt;/h4&gt;
&lt;p&gt;NVIDIA maintains a substantial advantage in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Software Maturity&lt;/strong&gt;: CUDA ecosystem is mature and well-supported&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI Framework Integration&lt;/strong&gt;: Native support across all major frameworks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer Tools&lt;/strong&gt;: Comprehensive profiling and debugging tools&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation&lt;/strong&gt;: Extensive, well-maintained documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AMD's advantages include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Open Source Approach&lt;/strong&gt;: More flexible licensing and community development&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unified Memory&lt;/strong&gt;: Simplified programming model for certain applications&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;: Potentially more cost-effective solutions&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Market Positioning&lt;/h4&gt;
&lt;p&gt;The AMD AI Max+ 395 occupies a unique position as a high-performance integrated solution, but software limitations significantly impact its competitiveness in AI-focused markets.&lt;/p&gt;
&lt;h3&gt;Use Case Suitability Analysis&lt;/h3&gt;
&lt;h4&gt;Recommended Use Cases&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;General Computing&lt;/strong&gt;: Excellent performance for traditional computational workloads&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Development Platforms&lt;/strong&gt;: Strong for general software development (non-AI)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge Computing&lt;/strong&gt;: Unified architecture benefits power-constrained deployments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Future AI Workloads&lt;/strong&gt;: When software ecosystem matures&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Not Recommended For&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Current AI Development&lt;/strong&gt;: gfx1151 compatibility issues are blocking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Production AI Inference&lt;/strong&gt;: Unreliable software support&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Machine Learning Research&lt;/strong&gt;: Limited framework compatibility&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time-Critical Projects&lt;/strong&gt;: Uncertain timeline for software fixes&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Large Language Model Performance and Stability&lt;/h3&gt;
&lt;h4&gt;Ollama LLM Inference Testing&lt;/h4&gt;
&lt;p&gt;Testing with Ollama reveals a mixed picture for LLM inference on the AMD AI Max+ 395 system. The platform successfully runs various models through CPU-based inference, though GPU acceleration faces significant challenges.&lt;/p&gt;
&lt;h5&gt;Performance Metrics&lt;/h5&gt;
&lt;p&gt;Testing with various model sizes revealed the following performance characteristics:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GPT-OSS 20B Model Performance:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prompt evaluation rate: 61.29 tokens/second&lt;/li&gt;
&lt;li&gt;Text generation rate: 8.99 tokens/second&lt;/li&gt;
&lt;li&gt;Total inference time: ~13 seconds for 117 tokens&lt;/li&gt;
&lt;li&gt;Memory utilization: ~54 GB VRAM usage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Llama 4 (67B) Model:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Successfully loads and runs&lt;/li&gt;
&lt;li&gt;Generation coherent and accurate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The system demonstrates adequate performance for smaller models (20B parameters and below) when running through Ollama, though performance significantly lags behind NVIDIA GPUs with proper CUDA acceleration. The large unified memory configuration (96 GB VRAM, deliberately maximized for this testing) allows loading of substantial models that would typically require multiple GPUs or extensive system RAM on other platforms. This conscious decision to allocate maximum memory to the GPU was specifically made to evaluate the system's potential for large language model workloads.&lt;/p&gt;
&lt;h4&gt;Critical Stability Issues with Large Models&lt;/h4&gt;
&lt;h5&gt;Driver Crashes with Advanced AI Workloads&lt;/h5&gt;
&lt;p&gt;Testing revealed severe stability issues when attempting to run larger models or when using AI-accelerated development tools:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Affected Scenarios:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Large Model Loading&lt;/strong&gt;: GPT-OSS 120B model causes immediate amdgpu driver crashes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI Development Tools&lt;/strong&gt;: Continue.dev with certain LLMs triggers GPU reset&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenAI Codex Integration&lt;/strong&gt;: Consistent driver failures with models exceeding 70B parameters&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;GPU Reset Events&lt;/h5&gt;
&lt;p&gt;System logs reveal frequent GPU reset events during AI workload attempts:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt; 1030.960155&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amdgpu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0000&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="nl"&gt;c5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00.0&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;amdgpu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GPU&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;begin&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt; 1033.972213&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amdgpu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0000&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="nl"&gt;c5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00.0&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;amdgpu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MODE2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reset&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt; 1034.002615&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amdgpu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0000&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="nl"&gt;c5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00.0&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;amdgpu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GPU&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;succeeded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;trying&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;resume&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt; 1034.003141&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;drm&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;VRAM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lost&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;due&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GPU&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt; 1034.037824&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amdgpu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0000&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="nl"&gt;c5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00.0&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;amdgpu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GPU&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;succeeded&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These crashes result in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Complete loss of VRAM contents&lt;/li&gt;
&lt;li&gt;Application termination&lt;/li&gt;
&lt;li&gt;Potential system instability requiring reboot&lt;/li&gt;
&lt;li&gt;Interrupted workflows and data loss&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Root Cause Analysis&lt;/h4&gt;
&lt;p&gt;The driver instability appears to stem from the same underlying issue as the PyTorch/ROCm incompatibility: &lt;strong&gt;immature driver support for the gfx1151 architecture&lt;/strong&gt;. The drivers struggle with:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Memory Management&lt;/strong&gt;: Large model allocations exceed driver's tested parameters&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute Dispatch&lt;/strong&gt;: Complex kernel launches trigger unhandled edge cases&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Power State Transitions&lt;/strong&gt;: Rapid load changes cause driver state machine failures&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Synchronization Issues&lt;/strong&gt;: Multi-threaded inference workloads expose race conditions&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Implications for AI Development&lt;/h4&gt;
&lt;p&gt;The combination of LLM testing results and driver stability issues reinforces that the AMD AI Max+ 395 system, despite impressive hardware specifications, remains unsuitable for production AI workloads. The platform shows promise for future AI applications once driver maturity improves, but current limitations include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unreliable Large Model Support&lt;/strong&gt;: Models over 70B parameters risk system crashes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited Tool Compatibility&lt;/strong&gt;: Popular AI development tools cause instability&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflow Interruptions&lt;/strong&gt;: Frequent crashes disrupt development productivity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Loss Risk&lt;/strong&gt;: VRAM resets can lose unsaved work or model states&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Future Outlook and Development Roadmap&lt;/h3&gt;
&lt;h4&gt;Short-term Expectations (3-6 months)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;ROCm updates likely to address gfx1151 compatibility&lt;/li&gt;
&lt;li&gt;PyTorch/TensorFlow support should improve&lt;/li&gt;
&lt;li&gt;Community-driven workarounds may emerge&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Medium-term Prospects (6-18 months)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Full AI framework support expected&lt;/li&gt;
&lt;li&gt;Optimization improvements for Strix Halo architecture&lt;/li&gt;
&lt;li&gt;Better documentation and developer resources&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Long-term Considerations (18+ months)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;AMD's commitment to open-source ecosystem should pay dividends&lt;/li&gt;
&lt;li&gt;Potential for superior price/performance ratios&lt;/li&gt;
&lt;li&gt;Growing developer community around ROCm platform&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Conclusions and Recommendations&lt;/h3&gt;
&lt;p&gt;The AMD AI Max+ 395 system represents impressive hardware engineering with its unified memory architecture, strong CPU performance, and substantial GPU compute capabilities. However, critical software ecosystem gaps, particularly the gfx1151 compatibility issues with PyTorch and ROCm, severely limit its immediate utility for AI and machine learning workloads.&lt;/p&gt;
&lt;h4&gt;Key Findings Summary&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Hardware Strengths:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Excellent CPU performance with 16 Zen 5 cores&lt;/li&gt;
&lt;li&gt;Innovative unified memory architecture with 96 GB addressable&lt;/li&gt;
&lt;li&gt;Strong integrated GPU with 40 compute units&lt;/li&gt;
&lt;li&gt;Efficient power management and thermal characteristics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Software Limitations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Critical gfx1151 architecture support gaps in ROCm ecosystem&lt;/li&gt;
&lt;li&gt;PyTorch integration completely broken for GPU acceleration&lt;/li&gt;
&lt;li&gt;Limited AI framework compatibility across the board&lt;/li&gt;
&lt;li&gt;Insufficient documentation for troubleshooting&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Market Position:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Competitive hardware specifications&lt;/li&gt;
&lt;li&gt;Unique integrated architecture advantages&lt;/li&gt;
&lt;li&gt;Significant software ecosystem disadvantages versus NVIDIA&lt;/li&gt;
&lt;li&gt;Uncertain timeline for compatibility improvements&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Purchasing Recommendations&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Buy If:&lt;/strong&gt;
- Primary use case is general computing or traditional HPC workloads
- Willing to wait 6-12 months for AI software ecosystem maturity
- Value open-source software development approach
- Need power-efficient integrated solution&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoid If:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Immediate AI/ML development requirements&lt;/li&gt;
&lt;li&gt;Production AI inference deployments planned&lt;/li&gt;
&lt;li&gt;Time-critical project timelines&lt;/li&gt;
&lt;li&gt;Require guaranteed software support&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Final Verdict&lt;/h4&gt;
&lt;p&gt;The AMD AI Max+ 395 system shows tremendous promise as a unified computing platform, but premature software ecosystem development makes it unsuitable for current AI workloads. Organizations should monitor ROCm development progress closely, as this hardware could become highly competitive once software support matures. For general computing applications, the system offers excellent performance and value, representing AMD's continued progress in processor design and integration.&lt;/p&gt;
&lt;p&gt;The AMD AI Max+ 395 represents a glimpse into the future of integrated computing platforms, but early adopters should be prepared for software ecosystem growing pains. As AMD continues investing in ROCm development and the open-source community contributes solutions, this platform has the potential to become a compelling alternative to NVIDIA's ecosystem dominance.&lt;/p&gt;</description><category>ai hardware</category><category>amd</category><category>benchmarks</category><category>gfx1151</category><category>gpu computing</category><category>machine learning</category><category>pytorch</category><category>rocm</category><category>ryzen ai</category><category>strix halo</category><guid>https://tinycomputers.io/posts/amd-ai-max%2B-395-system-review-a-comprehensive-analysis.html</guid><pubDate>Sun, 21 Sep 2025 20:25:28 GMT</pubDate></item></channel></rss>