<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Phoenix Labs</title>
    <description>The latest articles on DEV Community by Phoenix Labs (@thekillsquad007).</description>
    <link>https://dev.to/thekillsquad007</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4227973%2Fe0a0c3f2-7380-4555-8183-fc8ff1294ed7.png</url>
      <title>DEV Community: Phoenix Labs</title>
      <link>https://dev.to/thekillsquad007</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thekillsquad007"/>
    <language>en</language>
    <item>
      <title>Why upscaling old low-res video makes it look worse, and the ffmpeg filters that fix it</title>
      <dc:creator>Phoenix Labs</dc:creator>
      <pubDate>Thu, 05 Nov 2026 10:38:08 +0000</pubDate>
      <link>https://dev.to/thekillsquad007/why-upscaling-old-low-res-video-makes-it-look-worse-and-the-ffmpeg-filters-that-fix-it-4nnf</link>
      <guid>https://dev.to/thekillsquad007/why-upscaling-old-low-res-video-makes-it-look-worse-and-the-ffmpeg-filters-that-fix-it-4nnf</guid>
      <description>&lt;p&gt;If you've ever run an old 240p YouTube rip or a 2012 phone video through an AI upscaler, you've probably seen this: the output is bigger and sharper, and somehow looks &lt;em&gt;worse&lt;/em&gt;. Faces turn into mosaics. Flat walls grow a grid of tiles. Edges get a crunchy halo.&lt;/p&gt;

&lt;p&gt;I build a video restoration app, and this was one of the first quality bugs I had to understand properly. The short version: the upscaler is doing its job. It's sharpening exactly what you gave it, and what you gave it was mostly compression damage.&lt;/p&gt;

&lt;p&gt;This post covers what that damage is, how to tell which kind your clip has, and the ffmpeg filters I run &lt;em&gt;before&lt;/em&gt; any upscaler touches the video.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two kinds of old video, two different problems
&lt;/h2&gt;

&lt;p&gt;Most "old video" falls into one of two buckets, and they need opposite treatment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tape and DVD (480i / 576i).&lt;/strong&gt; VHS, Hi8, MiniDV captures and DVDs are usually &lt;em&gt;interlaced&lt;/em&gt;: each frame is two half-pictures (fields) captured a fraction of a second apart. On motion you get the comb pattern, thin horizontal stripes on anything moving. There's also analog noise: chroma smear, grain, tracking wobble.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low-res internet and phone video (≤ 288p, sometimes 360p and 480p).&lt;/strong&gt; Old uploads, early phone cameras, clips that have been re-shared five times. These are usually &lt;em&gt;progressive&lt;/em&gt;, so there's nothing to deinterlace, but they're crushed by compression. The encoder split each frame into 8×8 (or larger) blocks and threw away most of the detail inside each one. What's left is a picture made of tiles.&lt;/p&gt;

&lt;p&gt;The mistake I made at first was treating both the same way: a light deinterlace and a gentle denoise, then upscale. On tape that's fine. On a 240p download it's a disaster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the blocks get worse
&lt;/h2&gt;

&lt;p&gt;A neural upscaler is trained to find edges and texture and make them crisp. A block boundary &lt;em&gt;is&lt;/em&gt; an edge as far as the model is concerned: a hard, straight, high-contrast line. So it sharpens it.&lt;/p&gt;

&lt;p&gt;Upscale a 320×240 clip 4× and every 8×8 block becomes a 32×32 tile with a crisp outline. A gentle tape denoise doesn't help, because it's tuned for random grain and leaves the block grid alone.&lt;/p&gt;

&lt;p&gt;You can't fix this afterwards. Once the upscaler has decided the grid is real detail, it's baked into every frame. The fix has to happen before upscaling, while the blocks are still small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: find out what you have
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ffprobe&lt;/code&gt; tells you most of what you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; error &lt;span class="nt"&gt;-select_streams&lt;/span&gt; v:0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-show_entries&lt;/span&gt; &lt;span class="nv"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;width,height,field_order,codec_name,bit_rate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-of&lt;/span&gt; &lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;nw&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 input.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;field_order&lt;/code&gt; is &lt;code&gt;tt&lt;/code&gt;, &lt;code&gt;bb&lt;/code&gt;, &lt;code&gt;tb&lt;/code&gt; or &lt;code&gt;bt&lt;/code&gt; → interlaced. Treat it as tape.&lt;/li&gt;
&lt;li&gt;Height is 480, 486 or 576 with no field order reported → probably a tape or DVD capture that lost its metadata. Check a frame with motion for combing.&lt;/li&gt;
&lt;li&gt;Height is 288 or less → assume compression blocks, even if it looks "fine" at its native size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last rule is the one that matters. At 240p on a phone screen the blocks are barely visible. At 1080p they're the whole picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2a: the tape path (deinterlace + light denoise)
&lt;/h2&gt;

&lt;p&gt;For interlaced sources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; tape.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"yadif=0:-1:0,hqdn3d=2.0:1.5:3.0:2.5"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 12 &lt;span class="nt"&gt;-preset&lt;/span&gt; fast &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy &lt;span class="se"&gt;\&lt;/span&gt;
  tape_clean.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;yadif=0:-1:0&lt;/code&gt; deinterlaces to one frame per frame, auto-detects field order, and processes every frame. If you want smoother motion and can afford double the frames, &lt;code&gt;yadif=1&lt;/code&gt; (or &lt;code&gt;bwdif=1&lt;/code&gt;) outputs one frame per &lt;em&gt;field&lt;/em&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hqdn3d=2.0:1.5:3.0:2.5&lt;/code&gt; is a mild spatial + temporal denoise. The temporal part (the last two numbers) does most of the work on tape grain, because grain changes every frame and the picture doesn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the denoise mild. Tape footage that has been denoised hard looks waxy, and an upscaler will happily sharpen wax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2b: the low-res path (deblock first)
&lt;/h2&gt;

&lt;p&gt;For ≤ 288p progressive sources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; lowres.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"spp=5,deblock=filter=strong:block=8,hqdn3d=3.2:2.4:5.5:4.0"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 12 &lt;span class="nt"&gt;-preset&lt;/span&gt; fast &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy &lt;span class="se"&gt;\&lt;/span&gt;
  lowres_clean.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three filters, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;spp=5&lt;/code&gt;&lt;/strong&gt;, simple post-processing. It re-runs the DCT at shifted positions and averages the results, which is very good at removing block artifacts and ringing without blurring real edges much. Quality 5 is slow but worth it at these resolutions; the frames are tiny anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;deblock=filter=strong:block=8&lt;/code&gt;&lt;/strong&gt; smooths what's left along the 8×8 grid specifically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hqdn3d&lt;/code&gt; with stronger settings&lt;/strong&gt; than the tape path, to clean up the mosquito noise around edges that both of the above leave behind.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice there's no &lt;code&gt;yadif&lt;/code&gt;. These clips are progressive, and deinterlacing a progressive source just throws away vertical resolution you can't spare.&lt;/p&gt;

&lt;p&gt;The result will look &lt;em&gt;softer&lt;/em&gt; than the original at native size. That's expected and it's the point: you've removed fake detail (the grid) so the upscaler can work on real detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: keep intermediates lossless
&lt;/h2&gt;

&lt;p&gt;This one cost me more than I'd like to admit. If your pipeline extracts frames, processes them and re-encodes, possibly several times, don't use JPEG for the intermediate frames.&lt;/p&gt;

&lt;p&gt;I had frames passing between stages as JPEG to save disk space. Every stage re-compressed them, adding a fresh layer of 8×8 blocks, and the sharpening at the end then brought all of those blocks out. I'd carefully deblocked the source and then put the blocks back myself.&lt;/p&gt;

&lt;p&gt;Use PNG. You don't need maximum compression; PNG at level 1 is fast and still lossless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; lowres_clean.mp4 &lt;span class="nt"&gt;-compression_level&lt;/span&gt; 1 frames/%06d.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same idea for the intermediate &lt;em&gt;video&lt;/em&gt; files: &lt;code&gt;-crf 12&lt;/code&gt; (or lower) on the pre-clean output, as above, so you're not adding a new generation of compression before the upscaler sees it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: don't throw away pixels to save memory
&lt;/h2&gt;

&lt;p&gt;If you use a heavy model (a diffusion upscaler, for example) on a small GPU, it's tempting to downscale the input a bit more so it fits. Don't do this with a source that's already tiny.&lt;/p&gt;

&lt;p&gt;I had a memory fallback that dropped a 320-wide clip to 256 wide before inference. On paper that's a 20% cut. In practice, a 320×240 clip inferred at 256×192 and then stretched to 1080p is about a 5.6× stretch, and it looked pixelated no matter what came after. Shorter processing chunks at the native width were the better trade: slower, but the detail you start with is all the detail you'll ever have.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to expect
&lt;/h2&gt;

&lt;p&gt;With the right pre-clean, a 240p clip upscaled to 1080p looks like a soft but &lt;em&gt;clean&lt;/em&gt; video: real faces instead of mosaics, flat walls that are actually flat. It won't look like it was shot in HD, and nothing honest will make it look that way. The goal is "the best version of what was recorded," not new detail invented by a model.&lt;/p&gt;

&lt;p&gt;Tape footage, deinterlaced and lightly denoised, usually scales much better, because a 480i capture has more real information in it than its resolution suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Check &lt;code&gt;field_order&lt;/code&gt; and height with &lt;code&gt;ffprobe&lt;/code&gt; before choosing filters.&lt;/li&gt;
&lt;li&gt;Interlaced tape/DVD: &lt;code&gt;yadif&lt;/code&gt; + mild &lt;code&gt;hqdn3d&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;≤ 288p internet/phone video: &lt;code&gt;spp&lt;/code&gt; + &lt;code&gt;deblock&lt;/code&gt; + stronger &lt;code&gt;hqdn3d&lt;/code&gt;, and no deinterlacing.&lt;/li&gt;
&lt;li&gt;Deblock &lt;em&gt;before&lt;/em&gt; upscaling. You can't remove a grid the upscaler has already sharpened.&lt;/li&gt;
&lt;li&gt;Lossless intermediates (PNG, low CRF). Every JPEG generation adds blocks back.&lt;/li&gt;
&lt;li&gt;Don't downscale an already-tiny source to save memory.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I use all of this in &lt;a href="https://phoenixlabs.space/upscaler" rel="noopener noreferrer"&gt;Phoenix Upscaler&lt;/a&gt;, a desktop app that picks the right path for each clip automatically. I wrote more about the two common cases on the site: &lt;a href="https://phoenixlabs.space/old-phone-video" rel="noopener noreferrer"&gt;restoring old phone video&lt;/a&gt; and &lt;a href="https://phoenixlabs.space/vhs-restoration-software" rel="noopener noreferrer"&gt;VHS restoration&lt;/a&gt;. But the ffmpeg commands above work on their own, in front of any upscaler you like.&lt;/p&gt;

&lt;p&gt;If you've found filter settings that work better on a particular kind of source (Hi8, early-2000s webcam, 3GP phone video), I'd like to hear about them in the comments.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>python</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Running FlashVSR on an AMD GPU with ROCm (and what broke along the way)</title>
      <dc:creator>Phoenix Labs</dc:creator>
      <pubDate>Thu, 05 Nov 2026 08:10:20 +0000</pubDate>
      <link>https://dev.to/thekillsquad007/running-flashvsr-on-an-amd-gpu-with-rocm-and-what-broke-along-the-way-5bnm</link>
      <guid>https://dev.to/thekillsquad007/running-flashvsr-on-an-amd-gpu-with-rocm-and-what-broke-along-the-way-5bnm</guid>
      <description>&lt;p&gt;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 &lt;a href="https://github.com/LujiaJin/FlashVSR-Pro" rel="noopener noreferrer"&gt;FlashVSR-Pro&lt;/a&gt; fork, which wraps it in a usable &lt;code&gt;infer.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It also has a hard dependency on &lt;a href="https://github.com/mit-han-lab/Block-Sparse-Attention" rel="noopener noreferrer"&gt;Block-Sparse-Attention&lt;/a&gt;, a CUDA extension. No CUDA, no attention kernel, no model.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup that finally worked:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;RX 7800 XT, 16 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;Windows 11 + WSL2, Ubuntu 24.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ROCm&lt;/td&gt;
&lt;td&gt;7.2 system install inside WSL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch&lt;/td&gt;
&lt;td&gt;2.10.0+rocm7.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Precision&lt;/td&gt;
&lt;td&gt;fp16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. Use WSL2, not native Windows
&lt;/h2&gt;

&lt;p&gt;AMD now ships preview PyTorch wheels for Windows, and they import fine. &lt;code&gt;torch.cuda.is_available()&lt;/code&gt; returns &lt;code&gt;True&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Getting the GPU visible inside WSL took a few environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HSA_ENABLE_DXG_DETECTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ROCM_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/rocm-7.2.0
&lt;span class="nb"&gt;export &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.2.0/lib:&lt;span class="nv"&gt;$LD_LIBRARY_PATH&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HSA_OVERRIDE_GFX_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;11.0.1   &lt;span class="c"&gt;# gfx1101; adjust for your card&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One non-obvious fix: the PyTorch ROCm wheel bundles its own &lt;code&gt;libhsa-runtime64.so&lt;/code&gt;, 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 &lt;code&gt;rocminfo&lt;/code&gt; sees your card but PyTorch doesn't, check this first.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Make Block-Sparse-Attention optional
&lt;/h2&gt;

&lt;p&gt;FlashVSR imports the kernel at the top of &lt;code&gt;diffsynth/models/wan_video_dit.py&lt;/code&gt;. Step one is letting it fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;block_sparse_attn&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;block_sparse_attn_func&lt;/span&gt;
    &lt;span class="n"&gt;BLOCK_SPARSE_AVAILABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ModuleNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;block_sparse_attn_func&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;BLOCK_SPARSE_AVAILABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, where the attention mask is used, fall back to PyTorch's &lt;code&gt;scaled_dot_product_attention&lt;/code&gt; when the kernel isn't there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attention_mask&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;BLOCK_SPARSE_AVAILABLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rearrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b s (n d) -&amp;gt; b n s d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;num_heads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rearrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b s (n d) -&amp;gt; b n s d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;num_heads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rearrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b s (n d) -&amp;gt; b n s d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;num_heads&lt;/span&gt;&lt;span class="p"&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;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scaled_dot_product_attention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rearrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b n s d -&amp;gt; b s (n d)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;num_heads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's dense attention. It ignores FlashVSR's sparse locality mask completely, which costs speed, and more on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Turn on AOTriton, or SDPA will eat all your memory
&lt;/h2&gt;

&lt;p&gt;On ROCm, &lt;code&gt;scaled_dot_product_attention&lt;/code&gt; only uses a fused flash-attention kernel (AOTriton) on consumer RDNA3 cards if you ask for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;If you only remember one thing from this post, make it this variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The bug that turned every frame into noise
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;attn_mask&lt;/code&gt;. In theory that gives you the same attention pattern as the CUDA kernel, just slower.&lt;/p&gt;

&lt;p&gt;In practice every output frame was pure latent noise. Not slightly wrong, not blurry: static.&lt;/p&gt;

&lt;p&gt;The likely culprit is mask polarity (SDPA's boolean &lt;code&gt;attn_mask&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Memory: ROCm can't defragment, so don't let it fragment
&lt;/h2&gt;

&lt;p&gt;The NVIDIA advice for long diffusion runs is &lt;code&gt;PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True&lt;/code&gt;. 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".&lt;/p&gt;

&lt;p&gt;Three patches fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offload the DiT before decoding.&lt;/strong&gt; FlashVSR's TCDecoder runs after the diffusion step in each chunk, with the DiT still resident. Moving it off first gives the decoder room:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_models_to_device&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty_cache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;cur_frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TCDecoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode_video&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_models_to_device&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stream decoded frames to the CPU.&lt;/strong&gt; 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;non_blocking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Clean up after every streaming segment.&lt;/strong&gt; Delete the per-segment tensors and empty the cache before the next DiT step, so the allocator can hand back contiguous blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;cur_frames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur_latents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noise_pred_posi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur_LQ_frame&lt;/span&gt;
&lt;span class="n"&gt;gc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty_cache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these change the output. They just stop the run from dying halfway through.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Pick a resolution the card can actually do
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;fp16 over bf16.&lt;/strong&gt; bf16 was less reliable on RDNA3 in my runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DiT tiling&lt;/strong&gt; (&lt;code&gt;tile_dit&lt;/code&gt;) fits larger inputs into memory, but was about 8× slower. I only use it as a last resort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For low-res sources, keep the native width.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Don't trust WSL's idea of how much RAM you have
&lt;/h2&gt;

&lt;p&gt;Inside WSL, &lt;code&gt;psutil&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Two fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Raise the VM limit in &lt;code&gt;%UserProfile%\.wslconfig&lt;/code&gt; (&lt;code&gt;memory=12GB&lt;/code&gt; on a 32 GB host), then run &lt;code&gt;wsl --shutdown&lt;/code&gt; once.&lt;/li&gt;
&lt;li&gt;Pass the real host RAM in from the Windows side as an environment variable, and size things from that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  How fast is it?
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quality catch: diffusion warps on motion
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;What worked better for me was using it as a detail donor rather than the final picture:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run a conventional super-resolution model (Real-ESRGAN class) on the source. It's less detailed, but its geometry stays put.&lt;/li&gt;
&lt;li&gt;Run FlashVSR and line its output up with that base.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;Smooth that detail layer over time a little, so it doesn't shimmer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;If you want FlashVSR on an AMD card:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use WSL2 with a system ROCm install, and swap in the system &lt;code&gt;libhsa-runtime64.so&lt;/code&gt; if PyTorch can't see the GPU.&lt;/li&gt;
&lt;li&gt;Make Block-Sparse-Attention optional and fall back to dense SDPA.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Don't pass an expanded sparsity mask to SDPA unless you enjoy static.&lt;/li&gt;
&lt;li&gt;Offload the DiT before decoding, move decoded frames to the CPU, and clean up after every segment.&lt;/li&gt;
&lt;li&gt;Stay around 320 to 384 px input width on 16 GB, in fp16.&lt;/li&gt;
&lt;li&gt;Tell your code the real host RAM, not WSL's.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I packaged all of this into &lt;a href="https://phoenixlabs.space/upscaler" rel="noopener noreferrer"&gt;Phoenix Upscaler&lt;/a&gt;, 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 &lt;a href="https://phoenixlabs.space/upscaler-guide" rel="noopener noreferrer"&gt;setup guide&lt;/a&gt; covers what it installs, and there's a longer write-up on &lt;a href="https://phoenixlabs.space/restore-old-video" rel="noopener noreferrer"&gt;restoring old video in general&lt;/a&gt;. But everything above works on a plain FlashVSR-Pro checkout too.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
