<?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: Pratham Sharma</title>
    <description>The latest articles on DEV Community by Pratham Sharma (@pratham7711).</description>
    <link>https://dev.to/pratham7711</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%2F4017164%2F0caa3eaa-6954-4b9d-aa88-6eb565ed1fc7.png</url>
      <title>DEV Community: Pratham Sharma</title>
      <link>https://dev.to/pratham7711</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pratham7711"/>
    <language>en</language>
    <item>
      <title>Porting a 1,200-line persistent CUDA megakernel to Qwen3-TTS: ~25 ms to first audio chunk</title>
      <dc:creator>Pratham Sharma</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:59:58 +0000</pubDate>
      <link>https://dev.to/pratham7711/porting-a-1200-line-persistent-cuda-megakernel-to-qwen3-tts-25-ms-to-first-audio-chunk-l0i</link>
      <guid>https://dev.to/pratham7711/porting-a-1200-line-persistent-cuda-megakernel-to-qwen3-tts-25-ms-to-first-audio-chunk-l0i</guid>
      <description>&lt;p&gt;For a take-home assignment from e3 Group, I got a question I couldn't stop thinking about: how fast can text-to-speech &lt;em&gt;start speaking&lt;/em&gt; if you refuse to launch a thousand CUDA kernels?&lt;/p&gt;

&lt;p&gt;The answer, on an RTX 5090: the megakernel-backed decoder reached &lt;strong&gt;~25 ms time-to-first-chunk&lt;/strong&gt;, running at &lt;strong&gt;RTF 0.12&lt;/strong&gt; (it generates audio tokens ~8× faster than real time). Repo: &lt;a href="https://github.com/pratham7711/e3-megakernel-tts-takehome" rel="noopener noreferrer"&gt;e3-megakernel-tts-takehome&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Honest scope note before anything else: this was a take-home focused on the &lt;strong&gt;talker-decode path&lt;/strong&gt;. The codec stage was partly stubbed, so I make no claims about production-quality end-to-end audio — the interesting engineering (and the numbers) live in the decoder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: one kernel that never exits
&lt;/h2&gt;

&lt;p&gt;A normal LLM/TTS decode step launches a long chain of kernels — embedding lookup, attention, MLP, norms, sampling — and every launch pays fixed overhead. For big-batch training that overhead amortizes to nothing. For latency-critical, batch-of-one decoding it &lt;em&gt;is&lt;/em&gt; the cost: microseconds of launch latency and pipeline bubbles between every op, dozens of times per token.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;persistent megakernel&lt;/strong&gt; inverts this: launch one kernel that owns the GPU and loops over decode steps internally, keeping weights hot and skipping the launch/teardown cycle entirely. &lt;a href="https://github.com/AlpinDale" rel="noopener noreferrer"&gt;AlpinDale&lt;/a&gt; wrote a brilliant ~1,200-line persistent CUDA megakernel for Qwen decoding — my job was to make it decode a different model family.&lt;/p&gt;

&lt;h2&gt;
  
  
  Porting it to Qwen3-TTS-1.7B
&lt;/h2&gt;

&lt;p&gt;The megakernel was written for a Qwen text model. Qwen3-TTS-1.7B's talker looks similar on paper and disagrees in exactly the places a hand-tuned kernel cares about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model constants.&lt;/strong&gt; Hidden size, intermediate (MLP) size, and vocab size are baked into the kernel's tiling and launch configuration as compile-time constants. Retuning them isn't find-and-replace — the tile shapes have to keep the occupancy and shared-memory budget the kernel's design assumes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Untied embeddings.&lt;/strong&gt; The text model tied input embeddings and the output head; the TTS talker doesn't. That splits one weight buffer into two and changes what the final projection reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-section rotary embeddings (MRoPE).&lt;/strong&gt; Instead of one rotary frequency band over the whole head dimension, the TTS talker splits position encoding into sections. The kernel's inline RoPE math had to be reworked to apply per-section rotations without breaking its memory-access pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The debugging loop was the classic one: diff the megakernel's hidden states against a reference HuggingFace forward pass, layer by layer, until the divergence pointed at whichever assumption I hadn't found yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it talk end-to-end
&lt;/h2&gt;

&lt;p&gt;A decoder alone isn't a voice agent, so the pipeline runs through &lt;a href="https://github.com/pipecat-ai/pipecat" rel="noopener noreferrer"&gt;Pipecat&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deepgram&lt;/strong&gt; (speech-to-text) → &lt;strong&gt;Groq&lt;/strong&gt; (LLM reply) → &lt;strong&gt;Qwen3-TTS talker on the megakernel&lt;/strong&gt; (streaming audio tokens out chunk by chunk).&lt;/p&gt;

&lt;p&gt;Streaming is the point of the whole exercise. Time-to-first-chunk is what a human perceives as "it answered instantly" — and TTFC is exactly where per-step kernel-launch overhead hurts most, because the first chunk can't hide behind pipelining. ~25 ms to first chunk makes the TTS stage effectively free next to the LLM and network hops.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kernel launch overhead is a latency tax, not a throughput tax.&lt;/strong&gt; Persistent kernels are the difference in interactive, batch-of-one workloads; they're the wrong tool for big offline batches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Similar architecture" is a trap.&lt;/strong&gt; Tied vs. untied embeddings and single vs. multi-section RoPE are one-line differences in a config file and days of work in a hand-tuned kernel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer-by-layer diffing against a reference implementation&lt;/strong&gt; is the only sane way to port numerical code. Trust nothing end-to-end until every layer matches.&lt;/li&gt;
&lt;li&gt;Reading someone else's excellent CUDA (AlpinDale's kernel is genuinely instructive) teaches you more about GPU programming than writing mediocre CUDA of your own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/pratham7711/e3-megakernel-tts-takehome" rel="noopener noreferrer"&gt;github.com/pratham7711/e3-megakernel-tts-takehome&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Pratham Sharma, a software engineer (full-stack &amp;amp; AI) at Leegality. I write about the engineering behind things I actually shipped. Portfolio: &lt;a href="https://www.prathamsharma.in" rel="noopener noreferrer"&gt;prathamsharma.in&lt;/a&gt; · GitHub: &lt;a href="https://github.com/pratham7711" rel="noopener noreferrer"&gt;pratham7711&lt;/a&gt; · LinkedIn: &lt;a href="https://www.linkedin.com/in/pratham-sharma-a555b8207" rel="noopener noreferrer"&gt;pratham-sharma-a555b8207&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cuda</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>performance</category>
    </item>
    <item>
      <title>How I made a scroll-scrubbed video portfolio fast (Next.js 15 + GSAP + canvas)</title>
      <dc:creator>Pratham Sharma</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:58:33 +0000</pubDate>
      <link>https://dev.to/pratham7711/how-i-made-a-scroll-scrubbed-video-portfolio-fast-nextjs-15-gsap-canvas-1mj6</link>
      <guid>https://dev.to/pratham7711/how-i-made-a-scroll-scrubbed-video-portfolio-fast-nextjs-15-gsap-canvas-1mj6</guid>
      <description>&lt;p&gt;How do you ship a portfolio that plays 45 seconds of video, scrubbed by scroll, without shipping 12 MB of video?&lt;/p&gt;

&lt;p&gt;That was the problem with my portfolio, &lt;a href="https://www.prathamsharma.in" rel="noopener noreferrer"&gt;prathamsharma.in&lt;/a&gt;. It tells a story — a volleyball rally, three touches, three pitches — and every beat is footage that plays as you scroll. The first deployed version worked, but it was slow and glitchy: 12.8 MB of media fetched eagerly, 210 kB of First Load JS, and scroll jank on anything weaker than a desktop GPU.&lt;/p&gt;

&lt;p&gt;Here is exactly what I changed, with numbers. Everything below is live on the site right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Videos → WebP frame sequences on a canvas
&lt;/h2&gt;

&lt;p&gt;Scroll-scrubbing a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element is unreliable: seeking is async, keyframe intervals cause visible stutter, and mobile Safari fights you on autoplay and inline playback.&lt;/p&gt;

&lt;p&gt;So there are no video files at all. Each clip is extracted to 61 frames at 12 fps, 1280×720, and drawn onto a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;:&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; clip.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"fps=12,scale=1280:-2"&lt;/span&gt; /tmp/frames/%03d.jpg
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; /tmp/frames/&lt;span class="k"&gt;*&lt;/span&gt;.jpg&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;cwebp &lt;span class="nt"&gt;-q&lt;/span&gt; 68 &lt;span class="nt"&gt;-m&lt;/span&gt; 6 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"public/media/seq/dig/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="p"&gt;%.jpg&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.webp"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GSAP's ScrollTrigger drives a single &lt;code&gt;progress&lt;/code&gt; value from 0 to 1; the component maps it to a frame index and draws it. Scrubbing becomes deterministic — frame N is frame N, no seeking, no codec state.&lt;/p&gt;

&lt;p&gt;Two details that mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quality sweep, not guesswork.&lt;/strong&gt; I encoded the same sequence at several &lt;code&gt;cwebp&lt;/code&gt; quality levels and diffed visually. The source footage was already compressed, so &lt;code&gt;-q 68 -m 6&lt;/code&gt; was the sweet spot: ~20% smaller than the JPEG frames with no visible loss on moving, partially-masked footage. Total media went &lt;strong&gt;12.8 MB → 9.8 MB&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DPR cap.&lt;/strong&gt; The canvas backing store is capped at &lt;code&gt;devicePixelRatio 1.5&lt;/code&gt;. On a 3× phone screen you cannot see the difference on moving footage, and it's less than half the pixels to paint.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Load frames like you mean it
&lt;/h2&gt;

&lt;p&gt;Having 244 frames doesn't mean fetching 244 frames up front. Each sequence lazy-loads through two gates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;io&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;800px 0px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;IntersectionObserver&lt;/strong&gt; with an 800px margin starts a sequence only when the user is approaching it.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;requestIdleCallback&lt;/strong&gt; (with a &lt;code&gt;setTimeout&lt;/code&gt; fallback) defers decoding off the critical path.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;priority&lt;/code&gt; prop marks the hero sequence: the first beat loads immediately with default fetch priority, and every other sequence gets &lt;code&gt;img.fetchPriority = 'low'&lt;/code&gt; so it never competes with the hero for bandwidth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: the first paint needs one sequence, not four. The rest stream in while you read.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Code-split below the fold
&lt;/h2&gt;

&lt;p&gt;The page is one route, but it doesn't have to be one bundle. Everything below the fold became a &lt;code&gt;next/dynamic&lt;/code&gt; import — the acts, the marquee, the projects rail, the footer. Purely decorative client-only components (easter eggs, a flash-cut effect) got &lt;code&gt;ssr: false&lt;/code&gt; so they don't even render on the server.&lt;/p&gt;

&lt;p&gt;First Load JS: &lt;strong&gt;210 kB → 162 kB&lt;/strong&gt;. The above-the-fold experience — nav, hero sequence, custom cursor — is all that blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Cache immutable things immutably
&lt;/h2&gt;

&lt;p&gt;Frame sequences never change; when they do, the filename changes. So they're served with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: public, max-age=31536000, immutable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;via a &lt;code&gt;headers()&lt;/code&gt; entry in &lt;code&gt;next.config.ts&lt;/code&gt;. Second visit: zero media requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Stop animating when nothing moves
&lt;/h2&gt;

&lt;p&gt;Small one, but it shows up in profiles: the custom cursor ring used a permanent &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop, easing toward the pointer at rest — 60 fps of work to move 0 pixels. Now the loop kills itself when the ring settles within 0.1px and restarts on the next &lt;code&gt;mousemove&lt;/code&gt;. Idle CPU when the page is idle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Scroll-scrubbed video is a frame-sequence problem, not a video problem.&lt;/li&gt;
&lt;li&gt;Sweep encoder quality against your actual footage. The right number for masked, moving frames (68) is far below what you'd pick for stills.&lt;/li&gt;
&lt;li&gt;"Lazy" needs teeth: an observer margin, an idle callback, and an explicit fetch priority — any one alone still contends with the hero.&lt;/li&gt;
&lt;li&gt;Measure before and after. 12.8→9.8 MB and 210→162 kB are boring numbers that produce a non-boring difference on a mid-range phone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site is live at &lt;a href="https://www.prathamsharma.in" rel="noopener noreferrer"&gt;prathamsharma.in&lt;/a&gt; — scroll it with the network tab open and you can watch the staging happen.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Pratham Sharma, a software engineer (full-stack &amp;amp; AI) at Leegality. I write about the engineering behind things I actually shipped. Portfolio: &lt;a href="https://www.prathamsharma.in" rel="noopener noreferrer"&gt;prathamsharma.in&lt;/a&gt; · GitHub: &lt;a href="https://github.com/pratham7711" rel="noopener noreferrer"&gt;pratham7711&lt;/a&gt; · LinkedIn: &lt;a href="https://www.linkedin.com/in/pratham-sharma-a555b8207" rel="noopener noreferrer"&gt;pratham-sharma-a555b8207&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>performance</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
