<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Phoenix Labs]]></title><description><![CDATA[Phoenix Labs]]></description><link>https://phoenixlabs.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6aec3e039f337ab6b17a8444/6d335252-7fd8-4ed8-8d44-25e203dd61d5.png</url><title>Phoenix Labs</title><link>https://phoenixlabs.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 14:00:40 GMT</lastBuildDate><atom:link href="https://phoenixlabs.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Running FlashVSR on an AMD GPU with ROCm (and what broke along the way)]]></title><description><![CDATA[FlashVSR is one of the more interesting video super-resolution models right now. It's a one-step diffusion model built on Wan 2.1, it streams long videos…]]></description><link>https://phoenixlabs.hashnode.dev/running-flashvsr-on-an-amd-gpu-with-rocm-and-what-broke-along-the-way</link><guid isPermaLink="true">https://phoenixlabs.hashnode.dev/running-flashvsr-on-an-amd-gpu-with-rocm-and-what-broke-along-the-way</guid><category><![CDATA[AI]]></category><category><![CDATA[Python]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Phoenix Labs]]></dc:creator><pubDate>Thu, 05 Nov 2026 08:45:19 GMT</pubDate><content:encoded><![CDATA[<p>FlashVSR is one of the more interesting video super-resolution models right now. It's a one-step diffusion model built on Wan 2.1, it streams long videos in chunks, and on an NVIDIA card it's fast enough to be practical. I used the <a href="https://github.com/LujiaJin/FlashVSR-Pro">FlashVSR-Pro</a> fork, which wraps it in a usable <code>infer.py</code>.</p>
<p>It also has a hard dependency on <a href="https://github.com/mit-han-lab/Block-Sparse-Attention">Block-Sparse-Attention</a>, a CUDA extension. No CUDA, no attention kernel, no model.</p>
<p>I build a video restoration app that has to work on AMD cards too, so I spent a good chunk of August getting FlashVSR running on a Radeon RX 7800 XT (16 GB, RDNA3, gfx1101). It works now. This post is everything I had to change, in the order I hit it, so you don't have to rediscover it.</p>
<p><strong>Setup that finally worked:</strong></p>
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td>GPU</td>
<td>RX 7800 XT, 16 GB</td>
</tr>
<tr>
<td>Host</td>
<td>Windows 11 + WSL2, Ubuntu 24.04</td>
</tr>
<tr>
<td>ROCm</td>
<td>7.2 system install inside WSL</td>
</tr>
<tr>
<td>PyTorch</td>
<td>2.10.0+rocm7.0</td>
</tr>
<tr>
<td>Precision</td>
<td>fp16</td>
</tr>
</tbody></table>
<h2>1. Use WSL2, not native Windows</h2>
<p>AMD now ships preview PyTorch wheels for Windows, and they import fine. <code>torch.cuda.is_available()</code> returns <code>True</code>, your card shows up, and you feel great for about six seconds, which is roughly how long a full FlashVSR inference ran before it segfaulted. AMD's own notes mention intermittent crashes during inference on Windows, and that matched what I saw.</p>
<p>The same card inside WSL2 was stable, and a lot faster: a plain fp16 matmul benchmark gave me about 50 TFLOP/s in WSL against about 16 on native Windows.</p>
<p>Getting the GPU visible inside WSL took a few environment variables:</p>
<pre><code class="language-bash">export HSA_ENABLE_DXG_DETECTION=1
export ROCM_PATH=/opt/rocm-7.2.0
export LD_LIBRARY_PATH=/opt/rocm-7.2.0/lib:$LD_LIBRARY_PATH
export HSA_OVERRIDE_GFX_VERSION=11.0.1   # gfx1101; adjust for your card
</code></pre>
<p>One non-obvious fix: the PyTorch ROCm wheel bundles its own <code>libhsa-runtime64.so</code>, and that copy didn't detect the GPU through WSL's DXG layer for me. Replacing it with the system ROCm copy (keep a backup) fixed it. If <code>rocminfo</code> sees your card but PyTorch doesn't, check this first.</p>
<h2>2. Make Block-Sparse-Attention optional</h2>
<p>FlashVSR imports the kernel at the top of <code>diffsynth/models/wan_video_dit.py</code>. Step one is letting it fail:</p>
<pre><code class="language-python">try:
    from block_sparse_attn import block_sparse_attn_func
    BLOCK_SPARSE_AVAILABLE = True
except ModuleNotFoundError:
    block_sparse_attn_func = None
    BLOCK_SPARSE_AVAILABLE = False
</code></pre>
<p>Then, where the attention mask is used, fall back to PyTorch's <code>scaled_dot_product_attention</code> when the kernel isn't there:</p>
<pre><code class="language-python">if attention_mask is not None and not BLOCK_SPARSE_AVAILABLE:
    q = rearrange(q, "b s (n d) -&gt; b n s d", n=num_heads)
    k = rearrange(k, "b s (n d) -&gt; b n s d", n=num_heads)
    v = rearrange(v, "b s (n d) -&gt; b n s d", n=num_heads)
    x = F.scaled_dot_product_attention(q, k, v)
    x = rearrange(x, "b n s d -&gt; b s (n d)", n=num_heads)
</code></pre>
<p>That's dense attention. It ignores FlashVSR's sparse locality mask completely, which costs speed, and more on that below.</p>
<h2>3. Turn on AOTriton, or SDPA will eat all your memory</h2>
<p>On ROCm, <code>scaled_dot_product_attention</code> only uses a fused flash-attention kernel (AOTriton) on consumer RDNA3 cards if you ask for it:</p>
<pre><code class="language-bash">export TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1
</code></pre>
<p>Without it, PyTorch quietly falls back to the math backend, which builds the full attention matrix. For FlashVSR's sequence lengths that's tens of gigabytes. You won't get a helpful error, just an out-of-memory crash, or a process that dies with no traceback at all.</p>
<p>If you only remember one thing from this post, make it this variable.</p>
<h2>4. The bug that turned every frame into noise</h2>
<p>Dense attention throws away information the model was trained with, so the obvious next step was to keep it. I expanded FlashVSR's block-level sparsity mask into a token-level boolean mask and passed it to SDPA as <code>attn_mask</code>. In theory that gives you the same attention pattern as the CUDA kernel, just slower.</p>
<p>In practice every output frame was pure latent noise. Not slightly wrong, not blurry: static.</p>
<p>The likely culprit is mask polarity (SDPA's boolean <code>attn_mask</code> means "True = attend", and it's very easy to invert the block mask's meaning when expanding it), possibly combined with how the expanded mask interacts with the fused kernel. I didn't fully chase it down, because the plain dense path produced correct, clean output and the masked one wasn't faster on this card anyway: once you hand SDPA an arbitrary mask, you lose most of the fused-kernel advantage.</p>
<p>So the shipped path is dense, and mask expansion stays behind an off-by-default experiment flag. If you try this yourself and get noise, turn the mask off before you start debugging anything else.</p>
<h2>5. Memory: ROCm can't defragment, so don't let it fragment</h2>
<p>The NVIDIA advice for long diffusion runs is <code>PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True</code>. That wasn't available to me on ROCm, and FlashVSR's streaming loop fragments VRAM badly without it. Eventually even a 176 MiB allocation fails on a card with gigabytes "free".</p>
<p>Three patches fixed it.</p>
<p><strong>Offload the DiT before decoding.</strong> FlashVSR's TCDecoder runs after the diffusion step in each chunk, with the DiT still resident. Moving it off first gives the decoder room:</p>
<pre><code class="language-python">self.load_models_to_device([])
torch.cuda.empty_cache()
cur_frames = self.TCDecoder.decode_video(...)
self.load_models_to_device(['dit'])
</code></pre>
<p><strong>Stream decoded frames to the CPU.</strong> The decoder accumulates finished frames on the GPU and stacks them at the end. At 1280×960 that's around 3 GB of extra residency, scattered across the heap. Moving each finished frame to host memory as it's produced keeps the GPU heap compact:</p>
<pre><code class="language-python">out.append(xt.to("cpu", non_blocking=False))
</code></pre>
<p><strong>Clean up after every streaming segment.</strong> Delete the per-segment tensors and empty the cache before the next DiT step, so the allocator can hand back contiguous blocks:</p>
<pre><code class="language-python">del cur_frames, cur_latents, noise_pred_posi, cur_LQ_frame
gc.collect()
torch.cuda.empty_cache()
</code></pre>
<p>None of these change the output. They just stop the run from dying halfway through.</p>
<h2>6. Pick a resolution the card can actually do</h2>
<p>With all of the above, 16 GB comfortably handles 2× upscaling from an input around 320 to 384 pixels wide. A 512-wide input ran one streaming step and then died without a traceback. Some other knobs I tried:</p>
<ul>
<li><strong>fp16 over bf16.</strong> bf16 was less reliable on RDNA3 in my runs.</li>
<li><strong>DiT tiling</strong> (<code>tile_dit</code>) fits larger inputs into memory, but was about 8× slower. I only use it as a last resort.</li>
<li><strong>For low-res sources, keep the native width.</strong> For a 320×240 clip, I found it better to shorten the chunks (2 s down to 1 s) than to downscale to 256 wide first. Downscaling a source that small throws away detail you can't get back.</li>
</ul>
<h2>7. Don't trust WSL's idea of how much RAM you have</h2>
<p>Inside WSL, <code>psutil</code> reports the VM's memory cap, not the machine's. On a 32 GB host, WSL showed about 10 GB, and my memory-tuning code treated it as a small machine. It reloaded the model every two chunks to be safe, which roughly doubled the run time.</p>
<p>Two fixes:</p>
<ul>
<li>Raise the VM limit in <code>%UserProfile%\.wslconfig</code> (<code>memory=12GB</code> on a 32 GB host), then run <code>wsl --shutdown</code> once.</li>
<li>Pass the real host RAM in from the Windows side as an environment variable, and size things from that.</li>
</ul>
<p>Keeping one model process warm across chunks, and restarting it every 12 or so instead of every 2, was the single biggest speedup after getting attention right.</p>
<h2>How fast is it?</h2>
<p>My first working run took about 230 seconds for 2 seconds of 384×384 video upscaled to 768×768. After the memory and warm-worker fixes, a realistic number on the 7800 XT is about a minute of processing per second of 320 to 360p source.</p>
<p>That's fine for clips and highlights. A full 2-hour tape is not a reasonable job for this card, and dense attention is the reason. NVIDIA cards with the real sparse kernel are much faster.</p>
<h2>The quality catch: diffusion warps on motion</h2>
<p>Getting FlashVSR to run is half the story. On real footage, especially fast motion like a kick or a head turn, pure FlashVSR output tends to warp: edges drift a few pixels from where they are in the source, and faces can subtly change shape between frames.</p>
<p>What worked better for me was using it as a detail donor rather than the final picture:</p>
<ol>
<li>Run a conventional super-resolution model (Real-ESRGAN class) on the source. It's less detailed, but its geometry stays put.</li>
<li>Run FlashVSR and line its output up with that base.</li>
<li>Take only the high-frequency detail from FlashVSR, and only where the two agree at low frequency. Where they disagree (the warp), fall back to the base.</li>
<li>Smooth that detail layer over time a little, so it doesn't shimmer.</li>
</ol>
<p>The result keeps FlashVSR's texture and the source's shape. It also gets rid of the bright "sticker outline" that shows up around moving people when you sharpen a slightly misaligned diffusion output.</p>
<h2>Summary</h2>
<p>If you want FlashVSR on an AMD card:</p>
<ol>
<li>Use WSL2 with a system ROCm install, and swap in the system <code>libhsa-runtime64.so</code> if PyTorch can't see the GPU.</li>
<li>Make Block-Sparse-Attention optional and fall back to dense SDPA.</li>
<li>Set <code>TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1</code>.</li>
<li>Don't pass an expanded sparsity mask to SDPA unless you enjoy static.</li>
<li>Offload the DiT before decoding, move decoded frames to the CPU, and clean up after every segment.</li>
<li>Stay around 320 to 384 px input width on 16 GB, in fp16.</li>
<li>Tell your code the real host RAM, not WSL's.</li>
</ol>
<p>I packaged all of this into <a href="https://phoenixlabs.space/upscaler">Phoenix Upscaler</a>, a desktop app for restoring old video (tapes, DVDs, early phone footage), where the AMD generative mode sets up WSL and ROCm for you. The <a href="https://phoenixlabs.space/upscaler-guide">setup guide</a> covers what it installs, and there's a longer write-up on <a href="https://phoenixlabs.space/restore-old-video">restoring old video in general</a>. But everything above works on a plain FlashVSR-Pro checkout too.</p>
<p>If you get it running on a different RDNA card (a 7900 XTX, or anything RDNA2), I'd like to hear what resolution it handles.</p>
]]></content:encoded></item></channel></rss>