<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://flyinggiraffe.github.io/vidax-site/blog</id>
    <title>vidax Blog</title>
    <updated>2026-09-04T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://flyinggiraffe.github.io/vidax-site/blog"/>
    <subtitle>vidax Blog</subtitle>
    <icon>https://flyinggiraffe.github.io/vidax-site/img/favicon.ico</icon>
    <rights>Copyright © 2026 the vidax authors.</rights>
    <entry>
        <title type="html"><![CDATA[Sharding, parallelism, and JIT compilation on TPUs, with JAX]]></title>
        <id>https://flyinggiraffe.github.io/vidax-site/blog/hardware-and-sharding</id>
        <link href="https://flyinggiraffe.github.io/vidax-site/blog/hardware-and-sharding"/>
        <updated>2026-09-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A gentle background on Megatron-style tensor parallelism, DeepSpeed-Ulysses sequence parallelism, and how jax.jit really compiles a loop — illustrated with the "VAE decode hang" that turned out not to be a hang at all.]]></summary>
        <content type="html"><![CDATA[<p>Video DiTs are big in two different ways at once: the weights can be too
large for one chip, and — because self-attention runs over every patch in
every frame — a single forward pass's activations can be too large for one
chip even when the weights fit fine. This post is background on the two
sharding strategies vidax uses to deal with that (tensor parallelism,
sequence parallelism), and on a JAX-specific gotcha around <code>jax.jit</code> and
Python loops that's caused more real bugs, in more of vidax's models, than
anything else in this codebase.</p>
<!-- -->
<p>For the practical, day-to-day reference (which flags to pass, divisibility
rules, when to reach for which scheme), see the
<a class="" href="https://flyinggiraffe.github.io/vidax-site/docs/sharding/hardware-and-sharding">Hardware &amp; Sharding docs</a>. This post
is the "why" and the underlying mental model.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-shard-a-video-dit-at-all">Why shard a video DiT at all<a href="https://flyinggiraffe.github.io/vidax-site/blog/hardware-and-sharding#why-shard-a-video-dit-at-all" class="hash-link" aria-label="Direct link to Why shard a video DiT at all" title="Direct link to Why shard a video DiT at all" translate="no">​</a></h2>
<p>A text-to-video DiT patchifies a video into a sequence of tokens — one per
small spatio-temporal patch — and runs ordinary transformer self-attention
over that whole sequence. For a short image, that sequence might be a few
hundred tokens. For a few seconds of video at real resolution, it's tens of
thousands. Self-attention costs <code>O(sequence_length² × num_heads)</code>, so both
the weights <em>and</em> the activations of a video DiT can each, independently,
be too big for a single TPU chip's HBM (high-bandwidth memory — the
on-chip memory an accelerator computes against, analogous to a GPU's VRAM).
Two different sharding strategies fix two different halves of that
problem.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tensor-parallelism-megatron-style-shard-the-weights">Tensor parallelism (Megatron-style): shard the weights<a href="https://flyinggiraffe.github.io/vidax-site/blog/hardware-and-sharding#tensor-parallelism-megatron-style-shard-the-weights" class="hash-link" aria-label="Direct link to Tensor parallelism (Megatron-style): shard the weights" title="Direct link to Tensor parallelism (Megatron-style): shard the weights" translate="no">​</a></h2>
<p>The idea: split each layer's weight matrix across <code>N</code> chips along its
output dimension, so each chip only ever holds and computes with <code>1/N</code> of
the parameters. A minimal example, sharding a single dense layer's kernel
across a device mesh axis called <code>"tp"</code>:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> jax</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> jax</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">numpy </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> jnp</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> jax</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">sharding </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> Mesh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> NamedSharding</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> PartitionSpec </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> P</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">mesh </span><span class="token operator">=</span><span class="token plain"> Mesh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">jax</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">devices</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> axis_names</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"tp"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># Split the *output* features across chips: each chip holds</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># (in_features, out_features / tp_size) of the kernel.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">kernel_sharding </span><span class="token operator">=</span><span class="token plain"> NamedSharding</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">mesh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> P</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token boolean">None</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"tp"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">kernel </span><span class="token operator">=</span><span class="token plain"> jax</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">device_put</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">kernel</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> kernel_sharding</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">dense</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">x</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> kernel</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> x @ kernel  </span><span class="token comment" style="color:rgb(98, 114, 164)"># each chip computes its own output slice</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">y </span><span class="token operator">=</span><span class="token plain"> jax</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">jit</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">dense</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">x</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> kernel</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">  </span><span class="token comment" style="color:rgb(98, 114, 164)"># GSPMD inserts any needed communication</span><br></div></code></pre></div></div>
<p>This is "column-parallel": each chip computes a different slice of the
output, and no communication is needed until something needs the full
result back (a "row-parallel" layer right after it, which shards its
<em>input</em> dimension instead and sums the partial results with an
all-reduce). Chaining column-parallel → row-parallel layers, which is
exactly what an attention block's QKV projection → output projection and
an MLP's up-projection → down-projection look like, means the
all-reduce only has to happen once per block, not once per layer. This is
the <a href="https://arxiv.org/abs/1909.08053" target="_blank" rel="noopener noreferrer" class="">Megatron-LM</a> scheme, and it's what
<code>vidax.core.sharding.build_tpu_mesh</code>/<code>shard_wan_params</code> set up for every
DiT and text encoder in vidax.</p>
<p>Tensor parallelism shards weights (and whatever activations are direct
projections of them), but every chip still holds the <strong>full token
sequence</strong>. That's fine until the sequence itself — not the weights — is
what doesn't fit.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="sequence-parallelism-deepspeed-ulysses-shard-the-tokens">Sequence parallelism (DeepSpeed-Ulysses): shard the tokens<a href="https://flyinggiraffe.github.io/vidax-site/blog/hardware-and-sharding#sequence-parallelism-deepspeed-ulysses-shard-the-tokens" class="hash-link" aria-label="Direct link to Sequence parallelism (DeepSpeed-Ulysses): shard the tokens" title="Direct link to Sequence parallelism (DeepSpeed-Ulysses): shard the tokens" translate="no">​</a></h2>
<p>The idea here is the opposite: keep the full weights on every chip, but
give each chip only <code>1/N</code> of the <em>sequence</em>. The tricky part is
self-attention, which needs every token to see every other token — you
can't just compute attention locally on each chip's slice.
<a href="https://arxiv.org/abs/2309.14509" target="_blank" rel="noopener noreferrer" class="">DeepSpeed-Ulysses</a>'s answer is to
reshuffle data instead of tokens or weights: before attention, an
<code>all_to_all</code> redistributes so each chip ends up holding <em>every</em> token but
only <em>some</em> of the attention heads; attention runs locally per-head; a
second <code>all_to_all</code> reshuffles back to "every chip, its own token slice,
every head" for the rest of the block. Sketched out:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token comment" style="color:rgb(98, 114, 164)"># Before: each chip holds (local_seq_chunk, all_heads)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># After all_to_all: each chip holds (all_seq, local_head_chunk)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">kv_by_head </span><span class="token operator">=</span><span class="token plain"> jax</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">lax</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">all_to_all</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">kv_by_seq</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> axis_name</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"sp"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                                 split_axis</span><span class="token operator">=</span><span class="token number">0</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> concat_axis</span><span class="token operator">=</span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">out_by_head </span><span class="token operator">=</span><span class="token plain"> local_attention</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">kv_by_head</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">  </span><span class="token comment" style="color:rgb(98, 114, 164)"># full sequence, few heads</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">out_by_seq </span><span class="token operator">=</span><span class="token plain"> jax</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">lax</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">all_to_all</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">out_by_head</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> axis_name</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"sp"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                                 split_axis</span><span class="token operator">=</span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> concat_axis</span><span class="token operator">=</span><span class="token number">0</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>No chip ever recomputes another chip's tokens — it's a pure data
reshuffle around one attention op, with ordinary per-chip compute for
everything else (FFN, norms, modulation) running on that chip's own
sequence slice the whole time. This is what actually shrinks the
per-token activation memory that tensor parallelism can't touch.</p>
<p>The two compose: <code>build_tpu_mesh</code> builds a 3-axis <code>(dp, tp, sp)</code> mesh, and
vidax's larger models (Wan2.2's A14B) need both at once, since one axis
alone doesn't shard enough of <em>either</em> weights or activations to fit.</p>
<p>CogVideoX-1.5 is a second, trickier user of sequence parallelism. Its
block has no separate cross-attention: text and video tokens sit in <strong>one
joint self-attention sequence</strong>, so you can't just <code>all_to_all</code> the whole
thing — the 226 replicated text tokens would be reshuffled too. The port
(<code>sequence_parallel_joint_self_attention</code>) instead sends only the visual
q/k/v through the head↔sequence all-to-all and slices the replicated text
q/k/v down to each device's local head range before one local attention
call. That's what runs CogVideoX-1.5 at its native 1360×768 (~45k visual
tokens), whose per-block activations don't fit a v4 chip otherwise — and
for CogVideoX the two schemes are mutually exclusive, because the 5B DiT's
weights fit replicated per chip and the port never threads weight-sharding
through the sequence-parallel path.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="jit-compilation-what-jaxjit-actually-does-to-a-loop">JIT compilation: what <code>jax.jit</code> actually does to a loop<a href="https://flyinggiraffe.github.io/vidax-site/blog/hardware-and-sharding#jit-compilation-what-jaxjit-actually-does-to-a-loop" class="hash-link" aria-label="Direct link to jit-compilation-what-jaxjit-actually-does-to-a-loop" title="Direct link to jit-compilation-what-jaxjit-actually-does-to-a-loop" translate="no">​</a></h2>
<p><code>jax.jit</code> doesn't compile your Python function — it <strong>traces</strong> it once
(running it with abstract placeholder values to record what array
operations happen) and compiles <em>that trace</em> into a single fused program.
This matters a lot for any function containing a Python <code>for</code> loop: the
loop itself runs at trace time, in Python, so a 20-iteration loop doesn't
produce a 20-iteration compiled program — it produces one compiled program
containing 20 iterations' worth of operations, fully unrolled, all of
whose intermediate results the compiler may need to keep alive
simultaneously.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-vae-decode-hang-that-wasnt-a-hang">The VAE decode "hang" that wasn't a hang<a href="https://flyinggiraffe.github.io/vidax-site/blog/hardware-and-sharding#the-vae-decode-hang-that-wasnt-a-hang" class="hash-link" aria-label="Direct link to The VAE decode &quot;hang&quot; that wasn't a hang" title="Direct link to The VAE decode &quot;hang&quot; that wasn't a hang" translate="no">​</a></h3>
<p>Video VAE decoders in vidax decode a video's latent frames in chunks (a
handful of frames at a time), in a loop, because decoding the whole video
in one shot doesn't fit in memory. The very first version of this loop
looked entirely reasonable:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token decorator annotation punctuation" style="color:rgb(248, 248, 242)">@jax</span><span class="token decorator annotation punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token decorator annotation punctuation" style="color:rgb(248, 248, 242)">jit</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">decode_video</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">vae_params</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> latent_chunks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    frames </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> chunk </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> latent_chunks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">          </span><span class="token comment" style="color:rgb(98, 114, 164)"># traced, i.e. unrolled!</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        frames</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">append</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">vae</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token builtin" style="color:rgb(189, 147, 249)">apply</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">vae_params</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> chunk</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> jnp</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">concatenate</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">frames</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> axis</span><span class="token operator">=</span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>Run against a real ~20-chunk video, this appeared to hang for <strong>45+
minutes</strong>. It wasn't a deadlock — attaching <code>py-spy</code> to the running process
showed it stuck deep inside XLA's compiler. <code>@jax.jit</code> on the <em>outer</em>
function traces straight through the Python loop, so XLA was being asked
to compile one enormous program containing 20 chunks' worth of decoder
operations back to back, with no ability to free one chunk's intermediates
before the next chunk's began — a compile-time and memory problem
disguised as a runtime one.</p>
<p>The fix is to jit only the per-chunk function, and call it from an
ordinary (untraced) Python loop:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">decode_chunk </span><span class="token operator">=</span><span class="token plain"> jax</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">jit</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">vae</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token builtin" style="color:rgb(189, 147, 249)">apply</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">          </span><span class="token comment" style="color:rgb(98, 114, 164)"># compiled once, for one shape</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">frames </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> chunk </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> latent_chunks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">                </span><span class="token comment" style="color:rgb(98, 114, 164)"># a normal Python loop</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    frames</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">append</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">decode_chunk</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">vae_params</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> chunk</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">frames </span><span class="token operator">=</span><span class="token plain"> jnp</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">concatenate</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">frames</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> axis</span><span class="token operator">=</span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>Every chunk has the same shape, so <code>decode_chunk</code> is compiled exactly
once — 20 calls to an already-compiled program, not one enormous
from-scratch compilation. Just as importantly, each iteration's
intermediate buffers can now be freed once that iteration's call returns,
instead of all needing to coexist in one giant program's memory footprint.
The rule of thumb this generalizes to, used everywhere in vidax that has a
"do the same thing N times" loop (diffusion sampling steps,
<a class="" href="https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading">per-layer weight offloading</a>'s chunk loop): <strong>jit
the repeated unit of work, not the loop around it.</strong></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-cross-model-theme">A cross-model theme<a href="https://flyinggiraffe.github.io/vidax-site/blog/hardware-and-sharding#a-cross-model-theme" class="hash-link" aria-label="Direct link to A cross-model theme" title="Direct link to A cross-model theme" translate="no">​</a></h3>
<p>This isn't a Wan-specific fix — it's a pattern, and it keeps showing up
wherever a model has some form of chunked, repeated decode step.</p>
<p>LTX-2.5's optional "diffusion" VAE decoder (a transformer that runs local,
windowed attention over small 3D neighborhoods, rather than a single
deterministic conv decode) hit the exact same shape of problem, one level
deeper: a naive version that gathered every attention window at once
before running any math didn't just compile slowly, it OOM'd outright —
147GB of intermediate memory for a <em>tiny</em> test shape, since the local
window's volume multiplies memory by the product of all three of its
axes. Processing one slice of the window at a time in an ordinary Python
loop fixed the OOM but reintroduced the <em>other</em> half of this post's
lesson: at real resolution, the loop unrolled into a genuine 45+
minute-class compile, once per block, 24 blocks deep. The fix was
<code>jax.lax.scan</code> — JAX's built-in "repeat this step N times" primitive,
which (unlike a Python <code>for</code> loop inside <code>jax.jit</code>) compiles the
per-step body exactly once regardless of how many steps run, closing the
compile-time problem the same way jitting only the per-chunk function did
above. What remained after that was an ordinary memory-fit problem, closed
with the same tensor-parallel sharding already built for every other
model in vidax.</p>
<p>HunyuanVideo-1.5's VAE decoder ran into a version of this too — its
decode OOM'd on a full-resolution run with peak memory that didn't add up
to any single layer's own compute, the same "one fused trace doesn't free
each stage's intermediates before the next starts" signature. The fix that
landed is the same shape as all the others: on top of the spatial tiling
the reference VAE already does (latent-space H/W tiles with a linear
cross-fade of the overlaps), each tile is now decoded through a
<em>per-decoder-block staged pipeline</em> — <code>decode_stage_level_block</code> /
<code>decode_stage_level_upsample</code>, each its own <code>jax.jit</code> call — so one
stage's temporaries are freed before the next stage runs, instead of the
whole decode being one fused trace. That got the reference's real
121-frame/480p default decoding within budget.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary">Summary<a href="https://flyinggiraffe.github.io/vidax-site/blog/hardware-and-sharding#summary" class="hash-link" aria-label="Direct link to Summary" title="Direct link to Summary" translate="no">​</a></h2>
<table><thead><tr><th>Question</th><th>Tool</th></tr></thead><tbody><tr><td>Weights too big for one chip?</td><td>Tensor parallelism (shard weights, replicate sequence)</td></tr><tr><td>Activations too big for one chip, weights fit fine?</td><td>Sequence parallelism (shard sequence, replicate weights)</td></tr><tr><td>Both?</td><td>Compose both — separate mesh axes</td></tr><tr><td>A repeated per-chunk/per-step operation inside a loop?</td><td><code>jax.jit</code> the per-iteration function, call it from a plain Python loop — never <code>jax.jit</code> the loop itself</td></tr></tbody></table>
<hr>
<p>See the <a class="" href="https://flyinggiraffe.github.io/vidax-site/docs/sharding/hardware-and-sharding">Hardware &amp; Sharding docs</a> for
the practical, flag-by-flag reference, and
<a class="" href="https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading">Weight Offloading</a> for the next technique built on
top of this same "jit the unit, loop in Python" idea.</p>]]></content>
        <author>
            <name>vidax authors</name>
            <uri>https://github.com/FlyingGiraffe/vidax</uri>
        </author>
        <category label="Engineering Notes" term="Engineering Notes"/>
        <category label="Infrastructure" term="Infrastructure"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Understanding video diffusion architectures: DiT, MoT, and conditioning]]></title>
        <id>https://flyinggiraffe.github.io/vidax-site/blog/video-diffusion-architectures</id>
        <link href="https://flyinggiraffe.github.io/vidax-site/blog/video-diffusion-architectures"/>
        <updated>2026-09-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A background primer on diffusion transformers, text/image conditioning, and Mixture-of-Transformers — and a tour of what each model family in vidax actually uses.]]></summary>
        <content type="html"><![CDATA[<p>vidax now ports nine model families, and no two of them make exactly the
same architectural choices. Rather than write a separate deep-dive per
model, this is a tour of the handful of ideas that, combined differently,
account for most of the differences: the Diffusion Transformer (DiT) itself,
how text and image conditioning get into it, and Mixture-of-Transformers —
an architecture that isn't a DiT variant at all.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="diffusion-transformers-dit">Diffusion Transformers (DiT)<a href="https://flyinggiraffe.github.io/vidax-site/blog/video-diffusion-architectures#diffusion-transformers-dit" class="hash-link" aria-label="Direct link to Diffusion Transformers (DiT)" title="Direct link to Diffusion Transformers (DiT)" translate="no">​</a></h2>
<p>A video diffusion model's job is to predict, given a noisy video and a
timestep, either the noise or the "velocity" needed to denoise it a step
further. A DiT does this by patchifying the (spatio-temporal) video into a
sequence of tokens and running an ordinary transformer over them, with the
timestep injected into every block via <strong>adaptive layer norm (AdaLN)</strong>:
the timestep gets embedded and passed through a small MLP that predicts a
per-block scale/shift/gate, applied around that block's attention and MLP
sublayers. A minimal sketch:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">adaln_block</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">x</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> timestep_emb</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> attn</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> mlp</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> norm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)"># One small MLP per block predicts how this block should modulate</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)"># its own attention and MLP outputs, conditioned on the timestep.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    shift_attn</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> scale_attn</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> gate_attn</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> shift_mlp</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> scale_mlp</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> gate_mlp </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        modulation_mlp</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">timestep_emb</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    h </span><span class="token operator">=</span><span class="token plain"> norm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">x</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">*</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">1</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> scale_attn</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> shift_attn</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    x </span><span class="token operator">=</span><span class="token plain"> x </span><span class="token operator">+</span><span class="token plain"> gate_attn </span><span class="token operator">*</span><span class="token plain"> attn</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">h</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    h </span><span class="token operator">=</span><span class="token plain"> norm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">x</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">*</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">1</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> scale_mlp</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> shift_mlp</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    x </span><span class="token operator">=</span><span class="token plain"> x </span><span class="token operator">+</span><span class="token plain"> gate_mlp </span><span class="token operator">*</span><span class="token plain"> mlp</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">h</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> x</span><br></div></code></pre></div></div>
<p>This is the shape every DiT in vidax follows for its diffusion timestep —
Wan's, Cosmos-Predict2.5's — though the details (how many separate
modulation vectors, whether it's per-sample or per-token, the exact
RoPE convention for positions) all differ. One family, Cosmos3, skips AdaLN
entirely — more on that below.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="text-conditioning-cross-attention">Text conditioning: cross-attention<a href="https://flyinggiraffe.github.io/vidax-site/blog/video-diffusion-architectures#text-conditioning-cross-attention" class="hash-link" aria-label="Direct link to Text conditioning: cross-attention" title="Direct link to Text conditioning: cross-attention" translate="no">​</a></h2>
<p>Text almost always enters through ordinary <strong>cross-attention</strong>: video
tokens as the query, text-encoder tokens as the key/value, inserted as an
extra sublayer per block alongside self-attention:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">cross_attend_to_text</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">video_tokens</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> text_tokens</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> cross_attn</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    q </span><span class="token operator">=</span><span class="token plain"> project_q</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">video_tokens</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    k</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> v </span><span class="token operator">=</span><span class="token plain"> project_kv</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">text_tokens</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> cross_attn</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">q</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> k</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> v</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">  </span><span class="token comment" style="color:rgb(98, 114, 164)"># video tokens attend over text tokens</span><br></div></code></pre></div></div>
<p>What differs across models is <em>which</em> text encoder produces <code>text_tokens</code>
— a small T5 variant, or a multi-billion-parameter vision-language model —
and, for Cosmos-Predict2.5, <em>how many</em> of that encoder's layers get used
(concatenating hidden states from every decoder layer, not just the last).</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-alternative-put-conditioning-tokens-in-the-sequence-not-a-side-branch">The alternative: put conditioning tokens in the sequence, not a side branch<a href="https://flyinggiraffe.github.io/vidax-site/blog/video-diffusion-architectures#the-alternative-put-conditioning-tokens-in-the-sequence-not-a-side-branch" class="hash-link" aria-label="Direct link to The alternative: put conditioning tokens in the sequence, not a side branch" title="Direct link to The alternative: put conditioning tokens in the sequence, not a side branch" translate="no">​</a></h3>
<p>Not every model uses cross-attention for text at all. HunyuanVideo 1.0 and
1.5 instead <strong>concatenate</strong> their text tokens directly into the same
self-attention sequence the video tokens live in, tagged by a learned
per-source embedding so the model can tell which tokens came from where. In
1.5 that's two separate towers — a 7B vision-language model and a small
glyph/color encoder — plus, for I2V, SigLIP vision tokens; 1.0 is simpler,
a single Llama-3-8B tower's hidden states (with a separate pooled CLIP-L
vector folded into the AdaLN modulation, not the sequence). Either way
there's no separate cross-attention sublayer — conditioning happens because
every video token's self-attention already sees the text tokens sitting
right next to it in the sequence.</p>
<p>CogVideoX takes the same idea to its minimal form: <strong>one</strong> joint
self-attention per block over <code>[text (226 tokens); video]</code>, no
cross-attention sublayer and no second tower — just T5-XXL text embeddings
prepended to the video sequence, with RoPE applied to the video slice only.
Cosmos3's MoT pathway (below) is another variant: text and video tokens
share one sequence, just processed by different weight sets depending which
pathway they belong to.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="imagevideo-conditioning-i2v">Image/video conditioning (I2V)<a href="https://flyinggiraffe.github.io/vidax-site/blog/video-diffusion-architectures#imagevideo-conditioning-i2v" class="hash-link" aria-label="Direct link to Image/video conditioning (I2V)" title="Direct link to Image/video conditioning (I2V)" translate="no">​</a></h2>
<p>Conditioning on a starting image or clip (rather than pure text) shows up
in vidax in two genuinely different forms:</p>
<p><strong>Cross-attention to a separate image embedding.</strong> Wan2.1's I2V model
adds a <em>second</em> cross-attention branch per block, attending to a CLIP
embedding of the conditioning image — structurally identical to the text
cross-attention above, just with a different key/value source.</p>
<p><strong>Latent substitution.</strong> Wan2.2's TI2V-5B and Cosmos-Predict2.5 instead
VAE-encode the conditioning frames, then substitute those known latents
directly into the noisy input at the corresponding positions, at every
sampling step — paired with an extra mask channel marking which frames are
"given" versus "to be generated":</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">substitute_known_frames</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">x</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> known_latents</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> is_conditioning_frame</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)"># is_conditioning_frame: per-frame boolean mask</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> jnp</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">where</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">is_conditioning_frame</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> known_latents</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> x</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>No extra cross-attention branch needed — conditioning is expressed purely
through what's already in the input, plus the mask channel telling the
model which parts of that input to trust as ground truth.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="mixture-of-transformers-not-sparse-moe">Mixture-of-Transformers: not sparse MoE<a href="https://flyinggiraffe.github.io/vidax-site/blog/video-diffusion-architectures#mixture-of-transformers-not-sparse-moe" class="hash-link" aria-label="Direct link to Mixture-of-Transformers: not sparse MoE" title="Direct link to Mixture-of-Transformers: not sparse MoE" translate="no">​</a></h2>
<p>Cosmos3 is the one model family in vidax that isn't a DiT at all. It's an
omnimodal <strong>Mixture-of-Transformers (MoT)</strong> — a term that's easy to
confuse with sparse Mixture-of-Experts (MoE), but describes something
different. Sparse MoE routes individual tokens to a subset of expert
weights via a learned gate. MoT instead gives each <strong>modality</strong> its own
full weight set, used by every token of that modality, all the time — no
routing, no gate:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">mot_layer</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">tokens</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> is_text</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> text_weights</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> video_weights</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)"># every text token always uses text_weights; every video token</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)"># always uses video_weights -- decided by modality, not learned</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    text_out </span><span class="token operator">=</span><span class="token plain"> transformer_block</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">tokens</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> text_weights</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    video_out </span><span class="token operator">=</span><span class="token plain"> transformer_block</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">tokens</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> video_weights</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> jnp</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">where</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">is_text</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> text_out</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> video_out</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>Cosmos3 pairs this with a one-directional attention pattern: a causal
text ("understanding") pathway that self-attends normally, and a
full-attention "generation" (diffusion) pathway that attends over <em>both</em>
its own tokens and the text pathway's — so text conditions video, but
video never feeds back into text. And unlike every DiT above, there's no
AdaLN anywhere: the timestep is embedded and added directly into the noisy
video tokens once, then flows through ordinary pre-norm transformer blocks.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-each-model-in-vidax-actually-uses">What each model in vidax actually uses<a href="https://flyinggiraffe.github.io/vidax-site/blog/video-diffusion-architectures#what-each-model-in-vidax-actually-uses" class="hash-link" aria-label="Direct link to What each model in vidax actually uses" title="Direct link to What each model in vidax actually uses" translate="no">​</a></h2>
<table><thead><tr><th>Model</th><th>Architecture</th><th>Text conditioning</th><th>Image/video conditioning</th><th>Sampler</th></tr></thead><tbody><tr><td>Wan2.1</td><td>DiT</td><td>T5 (UMT5) cross-attention</td><td>Separate CLIP cross-attention branch</td><td>Euler</td></tr><tr><td>Wan2.2</td><td>DiT (A14B: 2-expert MoE by timestep)</td><td>T5 (UMT5) cross-attention</td><td>Latent substitution + mask (TI2V-5B)</td><td>Euler</td></tr><tr><td>Cosmos-Predict2.5</td><td>DiT, AdaLN-LoRA</td><td>Reason1 (7B VLM), all-layer concat</td><td>Latent substitution + mask + per-frame timestep</td><td>UniPC</td></tr><tr><td>Cosmos3</td><td>Mixture-of-Transformers, no AdaLN</td><td>Shared packed sequence (own MoT pathway)</td><td>Same packed-sequence mechanism as text</td><td>UniPC (Karras)</td></tr><tr><td>LTX-Video</td><td>DiT</td><td>T5-XXL cross-attention</td><td>Latent lerp + per-token effective-timestep clamp</td><td>Rectified Flow (LinearQuadratic Euler)</td></tr><tr><td>LTX-2.5</td><td>DiT, cross-attention AdaLN + gated attention</td><td>Gemma-4 (12B), via a learned connector</td><td>Per-token "denoise mask" (a generalized form of LTX-Video's clamp)</td><td>Ancestral (SDE) or plain Euler, by checkpoint</td></tr><tr><td>HunyuanVideo-1.5</td><td>Dual-stream/single-stream MMDiT</td><td>Qwen2.5-VL (7B) + byT5 glyph encoder, token concatenation</td><td>Channel-concat latent + SigLIP tokens, concatenated into the sequence</td><td>Rectified Flow (Euler)</td></tr><tr><td>HunyuanVideo (1.0)</td><td>Dual-stream/single-stream MMDiT (same family as 1.5)</td><td>Llama-3-8B token concatenation + pooled CLIP-L into AdaLN</td><td><code>token_replace</code>: latent substitution + a second "as-if-t=0" AdaLN vector for the first frame</td><td>Rectified Flow (Euler)</td></tr><tr><td>CogVideoX / 1.5</td><td>DiT, one joint <code>[text; video]</code> self-attention, per-sample AdaLN</td><td>T5-XXL, prepended to the sequence (no cross-attention)</td><td>Channel-concat image latent (→ 32 ch), every step</td><td>DDIM / DPM-Solver++ (v-prediction, zero-terminal-SNR)</td></tr></tbody></table>
<p>LTX-2.5 also has a second, genuinely different VAE decoder option: instead
of the usual deterministic convolutional decoder, <code>--vae_variant diffusion</code>
runs a small transformer that samples its own internal denoising step
directly on pixels — a different design from every other model's VAE here.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-few-things-worth-knowing">A few things worth knowing<a href="https://flyinggiraffe.github.io/vidax-site/blog/video-diffusion-architectures#a-few-things-worth-knowing" class="hash-link" aria-label="Direct link to A few things worth knowing" title="Direct link to A few things worth knowing" translate="no">​</a></h2>
<p>A handful of debugging lessons from getting these models running turned
out to be useful <em>diagnostic technique</em>, not just "here's a bug we found" —
worth keeping in mind for any diffusion model, not just the one that
surfaced them.</p>
<p><strong>A low-noise reconstruction probe separates "wrong noise-level handling"
from "wrong weights."</strong> When Cosmos-Predict2.5's early output came out as
a rigid grid of scrambled color blobs, the diagnostic that actually found
the cause wasn't staring at attention or RoPE code — it was encoding a
real photo with the VAE, adding a <em>small</em> amount of noise, running one
forward pass, and decoding. That reconstructed the photo almost perfectly
at low noise and degraded into the same grid-like garbage at high noise,
which pointed straight at <em>noise-level conditioning</em> (the model was being
told the wrong timestep, via a mistakenly-added preconditioning
transform) rather than the network's weights or architecture. Any
diffusion model producing texture-like or grid-like garbage is worth
testing this way before assuming the bug is somewhere more exotic.</p>
<p><strong>Generically-plausible-but-prompt-disconnected output usually means the
conditioning signal is weak, not that CFG/precision is wrong.</strong> LTX-2.5
once produced video that looked structurally fine but kept drifting
toward an unrelated recurring motif regardless of prompt or seed. The
cause was a rescale bug in the text-feature extractor that fed the DiT a
conditioning signal roughly 7x weaker than intended — not NaN, not
wrong-shaped, just quietly under-scaled, so the model's own generative
prior dominated. When output is "fine but ignores the prompt," it's worth
checking the conditioning path itself before spending time on CFG scale,
guidance rescale, or precision — a weak signal produces exactly the kind
of plausible-but-generic output that's easy to misread as a model-quality
ceiling instead of a bug.</p>
<p><strong>A bf16 text encoder is only safe if the padding is masked.</strong> CogVideoX
feeds its T5-XXL encoder no attention mask (the reference doesn't either),
so the full 226-token padded sequence is attended. T5-XXL's residual
stream reaches magnitudes around 1e5 — about two significant digits in
bfloat16 — and over 24 layers × 226 unmasked positions the error compounds
to 16–37% relative, with JAX-bf16 and torch-bf16 diverging from <em>each
other</em> by that much too. Per-block parity against an fp32 input stays at
~1e-7; it's purely the bf16 residual stream that blows up. LTX-Video never
hit this because it always passes the mask, so the pad positions get a
<code>-inf</code> bias and never enter the sum. The fix isn't bf16 parity — the
reference is unstable in bf16 too — it's running that one-time prompt
encode in float32 and casting only the output embeddings down.</p>
<p><strong>Prompt formatting requirements matter more for smaller models.</strong> Both
Cosmos3 checkpoints document that prompts should be "upsampled" into a
structured format (subjects, setting, lighting, cinematography, a
temporal caption) rather than passed as a short sentence. Nano (16B)
tolerates a short prompt reasonably well; Edge (4B) does not — the same
short prompt that's fine on Nano produces flat, oversaturated,
featureless output on Edge, with nothing in the pipeline (no error, no
broken shape) to flag that the prompt itself is the limiting factor.
Documented prompt-format requirements are easy to treat as polish; for a
smaller model they can be the difference between unusable and excellent
output.</p>
<hr>
<p>See <a class="" href="https://flyinggiraffe.github.io/vidax-site/blog/hardware-and-sharding">Sharding, parallelism, and JIT on TPUs</a>
and <a class="" href="https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading">Weight offloading</a> for how these
architectures actually get run at scale, or the model guides under
<a class="" href="https://flyinggiraffe.github.io/vidax-site/docs/models/wan2_1">Model Family Guides</a> for checkpoints and CLI usage.</p>]]></content>
        <author>
            <name>vidax authors</name>
            <uri>https://github.com/FlyingGiraffe/vidax</uri>
        </author>
        <category label="Engineering Notes" term="Engineering Notes"/>
        <category label="Modeling" term="Modeling"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Weight offloading: trading bandwidth for memory]]></title>
        <id>https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading</id>
        <link href="https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading"/>
        <updated>2026-09-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[What weight offloading is, why it exists (ZeRO-Offload), how vidax streams DiT weights from host RAM into HBM one chunk at a time, and what it actually costs — across five different model families, including one case where the same tool turned out to be the wrong one.]]></summary>
        <content type="html"><![CDATA[<p>Some of vidax's models don't fit fully on-chip at the resolutions their
reference implementations were trained at, even after sharding weights and
activations across every available chip
(<a class="" href="https://flyinggiraffe.github.io/vidax-site/blog/hardware-and-sharding">Sharding, parallelism, and JIT on TPUs</a>).
Weight offloading is the technique that closes that last gap: instead of
sharding, don't keep the weights resident on the accelerator at all — leave
them in host RAM and stream them in just in time.</p>
<!-- -->
<p>For the practical reference (which flag, which chunk size, per-model
guidance), see the <a class="" href="https://flyinggiraffe.github.io/vidax-site/docs/sharding/weight-offloading">Weight Offloading docs</a>.
This post covers the idea, the implementation, and the real cost of using
it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="background-what-weight-offloading-is">Background: what weight offloading is<a href="https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading#background-what-weight-offloading-is" class="hash-link" aria-label="Direct link to Background: what weight offloading is" title="Direct link to Background: what weight offloading is" translate="no">​</a></h2>
<p>An accelerator's HBM has to hold everything needed for the current
computation at once: weights, and everything the forward/backward pass
produces along the way (activations — for a diffusion transformer, mostly
the per-token modulation and attention tensors; the closest analogy for
readers coming from LLM serving is the KV cache, though diffusion models
don't have one). When the weights alone are a large fraction of that
budget, there's less room left for everything else.</p>
<p>Weight offloading trades HBM for host-to-device bandwidth: keep the full
weight tree in ordinary host (CPU) RAM — which is far more abundant and far
cheaper than HBM — and copy only the small slice currently needed onto the
accelerator, just before it's used, then let that HBM be reclaimed once
that slice's computation is done. It isn't a new idea:
<a href="https://arxiv.org/abs/2101.06840" target="_blank" rel="noopener noreferrer" class="">ZeRO-Offload</a> (and, closer to this exact
use case, HuggingFace <code>diffusers</code>'
<a href="https://huggingface.co/docs/diffusers/optimization/memory" target="_blank" rel="noopener noreferrer" class=""><code>enable_sequential_cpu_offload()</code></a>)
both do a version of the same trade. What makes it worth doing at all is
that host-to-device transfer bandwidth is normally much higher than the
compute time of the layer you're about to run — if the two overlap well,
streaming weights in "for free" while the previous layer computes costs
close to nothing.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-its-built-in-vidax">How it's built in vidax<a href="https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading#how-its-built-in-vidax" class="hash-link" aria-label="Direct link to How it's built in vidax" title="Direct link to How it's built in vidax" translate="no">​</a></h2>
<p>Every DiT in vidax repeats one block shape <code>num_layers</code> times — the
architectural property (<code>nn.scan</code>-style layer stacking) that makes
transformers scale by depth in the first place. That means a single
compiled program for "run one block/chunk of blocks" is reusable, verbatim,
for every chunk in the model — the compiler never needs to know <em>which</em>
chunk of weights it's holding, only that they match the shape/dtype it was
compiled for. The loop then looks like a direct application of the "jit the
unit, not the loop" rule from the
<a class="" href="https://flyinggiraffe.github.io/vidax-site/blog/hardware-and-sharding#jit-compilation-what-jaxjit-actually-does-to-a-loop">sharding/JIT post</a>:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">chunk_apply </span><span class="token operator">=</span><span class="token plain"> jax</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">jit</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">apply_one_chunk</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> donate_argnums</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">0</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> i </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">range</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">num_layers </span><span class="token operator">//</span><span class="token plain"> chunk_size</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    chunk_params </span><span class="token operator">=</span><span class="token plain"> jax</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">device_put</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">host_params</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">i</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> chunk_sharding</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">  </span><span class="token comment" style="color:rgb(98, 114, 164)"># stream in</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    x </span><span class="token operator">=</span><span class="token plain"> chunk_apply</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">chunk_params</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> x</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">                               </span><span class="token comment" style="color:rgb(98, 114, 164)"># compute</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)"># chunk_params' HBM is reclaimed once the next device_put reuses it</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)"># (donate_argnums lets the next chunk overwrite the same physical buffer)</span><br></div></code></pre></div></div>
<p><code>chunk_apply</code> is compiled exactly once, since every chunk has an identical
signature. <code>donate_argnums</code> tells JAX it's safe to reuse <code>chunk_params</code>'s
physical HBM for the <em>next</em> chunk's <code>device_put</code>, so the resident footprint
stays at one chunk's worth throughout a whole forward pass, not one chunk's
worth accumulating on top of the last. <code>--offload_chunk_size</code> controls how
many layers get grouped into one such chunk — a knob between "smallest
possible HBM footprint" (<code>chunk_size=1</code>) and "fewest, largest transfers"
(<code>chunk_size=num_layers</code>, i.e. the whole model).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-it-costs">What it costs<a href="https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading#what-it-costs" class="hash-link" aria-label="Direct link to What it costs" title="Direct link to What it costs" translate="no">​</a></h2>
<p>TPU matmuls accumulate in float32 internally regardless of input dtype, so
this technique's cost isn't numerical — it's a wall-clock race between
transfer time and compute time. On paper this should be close to free: a
14B-class model's weights, sharded across 4 chips at bf16, are only a few
GB per chip — well under a second to transfer at typical TPU host-to-device
bandwidth, against tens of seconds of compute per diffusion step.</p>
<p><strong>Measured, it wasn't free.</strong> Splitting that one transfer into 40 separate
per-layer transfers (one per layer of a 40-layer DiT) means whether they
overlap with the <em>previous</em> layer's compute — rather than serializing with
it — now matters a great deal. On real hardware, offloading measured
roughly <strong>5x slower per step</strong> than keeping everything resident, at the
chunk size that uses the least memory. Larger chunks (more layers streamed
together per transfer, at the cost of a bigger resident buffer) close some
but not all of that gap:</p>
<table><thead><tr><th style="text-align:right"><code>--offload_chunk_size</code></th><th style="text-align:right">Per-step (s)</th><th style="text-align:right">Peak HBM/chip (GB)</th></tr></thead><tbody><tr><td style="text-align:right">1 (smallest buffer)</td><td style="text-align:right">141.7</td><td style="text-align:right">15.2</td></tr><tr><td style="text-align:right">8</td><td style="text-align:right">131.3</td><td style="text-align:right">15.3</td></tr><tr><td style="text-align:right">20</td><td style="text-align:right">123.7</td><td style="text-align:right">23.0</td></tr><tr><td style="text-align:right">40 (whole model, largest buffer)</td><td style="text-align:right">111.3</td><td style="text-align:right">26.1</td></tr></tbody></table>
<p><strong>The takeaway: treat offloading as a correctness/memory-fit tool for
configs that don't fit any other way, not a free option to reach for by
default.</strong> It's the last lever to pull, after sharding, not the first.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-same-idea-five-different-forms">The same idea, five different forms<a href="https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading#the-same-idea-five-different-forms" class="hash-link" aria-label="Direct link to The same idea, five different forms" title="Direct link to The same idea, five different forms" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="wan21-fixing-an-oom-that-wasnt-where-it-looked">Wan2.1: fixing an OOM that wasn't where it looked<a href="https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading#wan21-fixing-an-oom-that-wasnt-where-it-looked" class="hash-link" aria-label="Direct link to Wan2.1: fixing an OOM that wasn't where it looked" title="Direct link to Wan2.1: fixing an OOM that wasn't where it looked" translate="no">​</a></h3>
<p>Wan2.1 14B needed offloading at native 720p not because the DiT's own
per-step compute didn't fit — a fully-sharded fp32 DiT comfortably fits
<em>while the sampling loop runs</em> — but because the DiT's weights were staying
HBM-resident for the entire script, competing with an unrelated phase's
memory: VAE decode right after sampling ends, VAE encode right before an
I2V sampling loop starts. Offloading fixes this by only ever making the
weights HBM-resident for the brief window each chunk actually needs them,
freeing that memory back up for the next phase.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="a14b-wan22-composing-with-sequence-parallelism">A14B (Wan2.2): composing with sequence parallelism<a href="https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading#a14b-wan22-composing-with-sequence-parallelism" class="hash-link" aria-label="Direct link to A14B (Wan2.2): composing with sequence parallelism" title="Direct link to A14B (Wan2.2): composing with sequence parallelism" translate="no">​</a></h3>
<p>Wan2.2's larger A14B model computes its timestep modulation <strong>per token</strong>
instead of per sample, which makes that one tensor alone multiple GB at
high resolution — pure <em>activation</em> memory that offloading, which only
targets weight memory, can't shrink no matter the chunk size. Fitting A14B
at real resolutions needed offloading <em>and</em> sequence parallelism at once,
which meant extending offloading to compose with a sharded token sequence
rather than assuming a full replica.</p>
<p>That combination surfaced a genuinely interesting bug, worth sharing as an
example of how subtle a sharding bug can look: outputs came out
blocky and spatially discontinuous, reproducible even at tiny resolutions.
The cause traced back to how a <em>row-parallel</em> layer (one where each chip
holds a slice of the input dimension, computes a partial output, and sums
partial outputs across chips) handles its bias. Each chip's local matmul
call adds the (replicated) bias <em>before</em> the cross-chip sum runs — so
summing <code>N</code> chips' outputs summed <code>N</code> copies of the bias instead of one.
Invisible with an all-zero bias (every synthetic test used one), but a real
per-layer error with trained biases, compounding across 40 layers into a
large enough numerical drift to visibly corrupt output. The fix subtracts
the bias before the cross-chip sum and adds exactly one copy back after —
a pattern worth remembering any time a row-parallel layer has a bias term
at all.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="cosmos-predict25-the-straightforward-case">Cosmos-Predict2.5: the straightforward case<a href="https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading#cosmos-predict25-the-straightforward-case" class="hash-link" aria-label="Direct link to Cosmos-Predict2.5: the straightforward case" title="Direct link to Cosmos-Predict2.5: the straightforward case" translate="no">​</a></h3>
<p>Cosmos-Predict2.5's 14B DiT needed the same technique to reach its
reference's full 93-frame default (previously reduced to 45 frames without
it) — a single DiT with no MoE-style expert switching to compose with,
so the implementation is close to the offloading loop shown above,
essentially unmodified. With offloading, the full 93 frames run
comfortably at 14.7GB peak HBM/chip.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="hunyuanvideo-10-two-chunk-pools-one-model">HunyuanVideo 1.0: two chunk pools, one model<a href="https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading#hunyuanvideo-10-two-chunk-pools-one-model" class="hash-link" aria-label="Direct link to HunyuanVideo 1.0: two chunk pools, one model" title="Direct link to HunyuanVideo 1.0: two chunk pools, one model" translate="no">​</a></h3>
<p>HunyuanVideo 1.0's 13B DiT needs offloading to fit the reference's real
129-frame / 720p default in HBM at all — a plain fit-the-sampling-loop case
like Wan2.1 and Cosmos above. The wrinkle is architectural: this DiT is a
dual-stream / single-stream MMDiT, 20 "double-stream" blocks followed by 40
"single-stream" blocks, and the two block types have <strong>different parameter
shapes</strong>. One chunk pool can't stream both. So <code>--offload_dit_weights</code>
runs <em>two</em> independent chunk loops — <code>--offload_chunk_size_double</code> (default
20, i.e. all double blocks in one chunk) and <code>--offload_chunk_size_single</code>
(default 40) — each its own separately-compiled <code>chunk_apply</code>, streamed and
freed in sequence. It's the same "jit the unit, stream one chunk at a time"
machinery as every other model here, just instantiated once per block
family instead of once for the whole stack. The per-step cost is dominated
by this offloading tax, not the implementation — HunyuanVideo-1.5, the same
block family with <em>zero</em> offloading, is already the slowest non-offloaded
row in vidax's benchmark table.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="ltx-25-offloading-for-a-reason-no-other-model-needed-it">LTX-2.5: offloading for a reason no other model needed it<a href="https://flyinggiraffe.github.io/vidax-site/blog/weight-offloading#ltx-25-offloading-for-a-reason-no-other-model-needed-it" class="hash-link" aria-label="Direct link to LTX-2.5: offloading for a reason no other model needed it" title="Direct link to LTX-2.5: offloading for a reason no other model needed it" translate="no">​</a></h3>
<p>LTX-2.5's 22B DiT needed <code>--offload_dit_weights</code> to reach its reference
resolution too, but for a genuinely different reason than every case
above: <strong>its weights were never the bottleneck.</strong> Fully resident, they're
only ~6.6GB/chip — comfortable headroom. What actually didn't fit was
activation memory from tracing the entire 48-block forward pass as one
fused <code>jax.jit</code> program, which — confirmed by measurement, not assumed —
scaled almost linearly with block count instead of staying roughly
constant, meaning per-block intermediates weren't being freed across
blocks. Offloading fixes this as a side effect: splitting the block loop
into <code>--offload_chunk_size</code>-sized separately-compiled chunks bounds peak
activation memory to one chunk's worth, by construction, independent of
whether weight streaming itself was ever needed. <code>--offload_chunk_size 8</code>
(the largest divisor of 48 that fits) got the reference's full
1216×704×121 resolution running at both checkpoints.</p>
<p><strong>A useful contrast, though: LTX-2.5's second, optional VAE decoder — a
transformer that runs local windowed attention instead of an ordinary
conv decode — hit a real memory ceiling of its own that offloading
<em>didn't</em> fix.</strong> Isolated single-block profiling suggested each block
needed only ~12GB, comfortably under budget, but the real multi-device
pipeline measured over 32GB for the same block — a genuine, still
unexplained gap between isolated and in-context profiling. Chunking
further didn't move the number at all. What did was reaching for a tool
this repo already had a working pattern for: Megatron-style tensor
parallelism across the same mesh the DiT and text encoder already use,
which dropped peak memory to a comfortable 14.76GB/chip. <strong>The lesson
worth generalizing: not every "doesn't fit" problem is a streaming
problem.</strong> Offloading trades HBM for bandwidth by keeping less resident at
once; sharding trades HBM for communication by splitting the work itself
across chips. When one doesn't move the number, it's worth checking
whether the other, already-built machinery applies instead of continuing
to tune the first.</p>
<hr>
<p>See <a class="" href="https://flyinggiraffe.github.io/vidax-site/blog/hardware-and-sharding">Sharding, parallelism, and JIT on TPUs</a>
for the mesh/JIT background this builds on, and the
<a class="" href="https://flyinggiraffe.github.io/vidax-site/docs/sharding/weight-offloading">Weight Offloading docs</a> for the
practical, per-model reference.</p>]]></content>
        <author>
            <name>vidax authors</name>
            <uri>https://github.com/FlyingGiraffe/vidax</uri>
        </author>
        <category label="Engineering Notes" term="Engineering Notes"/>
        <category label="Infrastructure" term="Infrastructure"/>
    </entry>
</feed>