<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Diffio AI Blog</title>
    <link>https://diffio.ai/blog/</link>
    <description>Speech restoration research, inference engineering, and historic recordings restored with Diffio.</description>
    <language>en-us</language>
    <lastBuildDate>Tue, 03 Nov 2026 09:49:22 GMT</lastBuildDate>
    <atom:link href="https://diffio.ai/blog/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>Hear General Pershing&apos;s 1918 message from the battlefields of France, restored</title>
      <link>https://diffio.ai/blog/pershing-1918-battlefields-of-france-restored/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/pershing-1918-battlefields-of-france-restored/</guid>
      <pubDate>Mon, 02 Nov 2026 08:40:45 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>Audio restoration</category>
      <category>History</category>
      <description>A 33-second disc General John J. Pershing recorded in France in March 1918, restored for Veterans Day, with the original alongside.</description>
      <media:content url="https://diffio.ai/media/blog/pershing-1918-battlefields-of-france-restored/feature.webp" medium="image"/>
      <content:encoded><![CDATA[<p>In March 1918, General John J. Pershing, commander of the American Expeditionary Forces, spoke 33 seconds into a recording horn in France. The Library of Congress catalog records the place as the American field headquarters on the Picardy battlefield. The Nation's Forum, a project that recorded American leaders during the First World War, issued the speech on a disc pressed by Columbia, with Ambassador James W. Gerard on the other side. The Library of Congress notes that this was the one Nation's Forum record sold commercially.</p>
<p>The Library of Congress holds a copy pressed from the original master and has digitized it for its collection &quot;American Leaders Speak: Recordings from World War I.&quot; It is an acoustic recording, made without a microphone, and the noise of the disc sits on top of Pershing's voice. For Veterans Day, 108 years after the armistice of November 11, 1918, we ran it through Diffio AI, our speech restoration tool. This page puts the original and the restoration side by side.</p>
<h2 id="listen-the-1918-recording-as-it-survives">Listen: the 1918 recording as it survives</h2>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/pershing-1918-battlefields-of-france-restored/pershing-1918-original.mp3" aria-label="Listen: the 1918 recording as it survives"><a href="https://diffio.ai/media/blog/pershing-1918-battlefields-of-france-restored/pershing-1918-original.mp3">Listen: the 1918 recording as it survives</a></audio></figure>

<h2 id="listen-the-same-recording-restored-with-diffio">Listen: the same recording, restored with Diffio</h2>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/pershing-1918-battlefields-of-france-restored/pershing-1918-restored.mp3" aria-label="Listen: the same recording, restored with Diffio"><a href="https://diffio.ai/media/blog/pershing-1918-battlefields-of-france-restored/pershing-1918-restored.mp3">Listen: the same recording, restored with Diffio</a></audio></figure>

<p>Both files are the whole disc, matched for loudness, so what changes is the noise and the voice, not the volume. What Pershing says:</p>
<blockquote>
<p>&quot;Three thousand miles from home, an American army is fighting for you. Everything you hold worthwhile is at stake. Only the hardest blows can win against the enemy we are fighting. Invoking the spirit of our forefathers, the army asks your unshrinking support, to the end that the high ideals for which America stands may endure upon the earth.&quot;</p>
</blockquote>
<p>The Library of Congress catalog quotes the phrase as &quot;unshrinking support.&quot; On both the original and the restoration, a speech recognition model hears &quot;unflinching,&quot; which shows how hard the word is to make out on the disc. We have kept the catalog's wording.</p>
<h2 id="watch-before-and-after">Watch: before and after</h2>
<figure class="blog-figure blog-figure--video"><video controls playsinline preload="metadata" src="https://diffio.ai/media/blog/pershing-1918-battlefields-of-france-restored/pershing-1918-before-after.mp4" poster="https://diffio.ai/media/blog/pershing-1918-battlefields-of-france-restored/pershing-1918-before-after-poster.webp" width="1280" height="720" aria-label="Watch: before and after"><a href="https://diffio.ai/media/blog/pershing-1918-battlefields-of-france-restored/pershing-1918-before-after.mp4">Watch: before and after</a></video></figure>

<h2 id="what-changed">What changed</h2>
<p>Diffio restores recorded speech. It separates the voice from everything else in the recording and rebuilds it so it sounds closer to a clean microphone. On this disc the surface noise drops away and Pershing's voice comes forward.</p>
<p>We checked the result the same way we check our own work: we ran a speech recognition model (Whisper) on the original and on the restoration. On the original it heard the last line as &quot;they endure upon the earth&quot;; on the restoration it heard &quot;may endure upon the earth,&quot; which is what Pershing said. Everything else matched.</p>
<p>One honest note from the process. On our first pass the restoration lost the opening words, &quot;thousand miles from home,&quot; because the speech starts in the very first second of the file. Adding two seconds of silence before the recording fixed it, and the version on this page is that second pass. If you restore a recording that starts mid-word, give it a moment of lead-in.</p>
<p>The models are generative, which is why the result can be so much clearer than a noise filter manages on audio this old. It is also why we publish the original next to the restoration. Treat the restored file as a listening copy: the original stays exactly as the Library of Congress digitized it, and anyone quoting Pershing should check the words against it.</p>
<h2 id="more-voices-from-the-same-years">More voices from the same years</h2>
<p>Earlier this fall we restored <a href="https://diffio.ai/blog/harding-1921-hoboken-address-restored/">Warren G. Harding's 1921 Hoboken address for the American war dead</a>, which he recorded for Victor in 1922, and a set of <a href="https://diffio.ai/blog/early-presidents-voices-restored/">early presidential voices from 1908 to 1922</a>. The same kind of restoration works on recordings much closer to home: a grandfather's cassette about his service, a reel-to-reel of a Veterans Day service, an oral history taped in a noisy hall. You can <a href="https://diffio.ai/">try Diffio on three uploads without a card</a>.</p>
<h2 id="for-journalists-educators-and-museums">For journalists, educators and museums</h2>
<p>The Library of Congress records that this disc was sold commercially in 1918, and U.S. sound recordings published before 1923 entered the public domain in 2022. The restored audio and the video on this page are free to use for press, classroom and museum work. Write to me at nathan.harmon@trydiffio.com if you'd like WAV files. Please credit &quot;Audio: Gen. John J. Pershing, 'From the Battlefields of France,' Nation's Forum (1918), Library of Congress; restored with Diffio AI (diffio.ai)&quot;. The original is at the <a href="https://www.loc.gov/item/00585648/">Library of Congress</a>.</p>
<p><em>Nathan Harmon is the founder of Diffio AI, based in Colorado Springs.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Hear six early presidents in their own voices, restored (1908-1922)</title>
      <link>https://diffio.ai/blog/early-presidents-voices-restored/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/early-presidents-voices-restored/</guid>
      <pubDate>Sun, 01 Nov 2026 04:34:22 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>Audio restoration</category>
      <category>History</category>
      <description>Taft in 1908, Roosevelt, Wilson and Taft in 1912, Coolidge and FDR in 1920, Harding in 1921: public domain phonograph speeches, each original next to its restoration.</description>
      <content:encoded><![CDATA[<p>Before radio, the closest most Americans came to hearing a president was a phonograph record. Between 1908 and 1922 candidates and presidents recorded short speeches for Victor and for the Nation's Forum series, made acoustically, by speaking into a horn, with no microphone. Those discs survive, but the voices sit under a century of hiss, crackle and horn resonance.</p>
<p>We restored them with Diffio AI, our speech restoration tool. This page collects six voices from those years, from William Howard Taft in 1908 to Franklin D. Roosevelt in 1920. Each pair below is the same passage from the same disc, original first, matched for loudness, and each restored excerpt was checked word for word against the original. All of the recordings were published before 1923, so they are in the public domain in the United States.</p>
<h2 id="william-howard-taft-1908-functions-of-the-next-administration">William Howard Taft, 1908: &quot;Functions of the Next Administration&quot;</h2>
<p>Taft recorded a set of campaign speeches for Victor at Hot Springs, Virginia, on August 5, 1908, while he was the Republican nominee. In this one he promises to carry on Theodore Roosevelt's fight against lawbreaking corporations:</p>
<blockquote>
<p>&quot;The chief function of the next administration, in my judgment, is distinct from, and a progressive development of, that which has been performed by President Roosevelt. The chief function of the next administration is to complete and perfect the machinery by which these standards may be maintained, by which the lawbreakers may be promptly restrained and punished, but which shall operate with sufficient accuracy and dispatch to interfere with legitimate business as little as possible.&quot;</p>
</blockquote>
<p><strong>Original (Victor 31709, recorded August 5, 1908)</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/early-presidents-voices-restored/taft-1908-original.mp3" aria-label="Original (Victor 31709, recorded August 5, 1908), William Howard Taft, 1908: &quot;Functions of the Next Administration&quot;"><a href="https://diffio.ai/media/blog/early-presidents-voices-restored/taft-1908-original.mp3">Original (Victor 31709, recorded August 5, 1908), William Howard Taft, 1908: &quot;Functions of the Next Administration&quot;</a></audio></figure>

<p><strong>Restored with Diffio</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/early-presidents-voices-restored/taft-1908-restored.mp3" aria-label="Restored with Diffio, William Howard Taft, 1908: &quot;Functions of the Next Administration&quot;"><a href="https://diffio.ai/media/blog/early-presidents-voices-restored/taft-1908-restored.mp3">Restored with Diffio, William Howard Taft, 1908: &quot;Functions of the Next Administration&quot;</a></audio></figure>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/early-presidents-voices-restored/victor-31709-taft-label.webp" alt="Taft's disc, Victor 31709. Image: Library of Congress National Jukebox." width="1400" height="1408" loading="lazy" decoding="async"><figcaption>Taft's disc, Victor 31709. Image: Library of Congress National Jukebox.</figcaption></figure>

<h2 id="roosevelt-wilson-and-taft-1912">Roosevelt, Wilson and Taft, 1912</h2>
<p>In the fall of 1912 all three candidates recorded for Victor. Roosevelt, running as a Progressive, answered the trusts and party bosses: &quot;They dread you, the people.&quot; Wilson took apart Roosevelt's plan to regulate monopoly, and Taft, now the sitting president, asked &quot;Who are the people?&quot; All three restorations, with the disc labels and a short video, are on <a href="https://diffio.ai/blog/1912-election-roosevelt-wilson-taft-voices-restored/">our 1912 election page</a>. Here is Roosevelt:</p>
<p><strong>Original (Victor 35250-A, recorded September 22, 1912)</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/early-presidents-voices-restored/roosevelt-1912-original.mp3" aria-label="Original (Victor 35250-A, recorded September 22, 1912), Roosevelt, Wilson and Taft, 1912"><a href="https://diffio.ai/media/blog/early-presidents-voices-restored/roosevelt-1912-original.mp3">Original (Victor 35250-A, recorded September 22, 1912), Roosevelt, Wilson and Taft, 1912</a></audio></figure>

<p><strong>Restored with Diffio</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/early-presidents-voices-restored/roosevelt-1912-restored.mp3" aria-label="Restored with Diffio, Roosevelt, Wilson and Taft, 1912"><a href="https://diffio.ai/media/blog/early-presidents-voices-restored/roosevelt-1912-restored.mp3">Restored with Diffio, Roosevelt, Wilson and Taft, 1912</a></audio></figure>

<h2 id="calvin-coolidge-1920-law-and-order">Calvin Coolidge, 1920: &quot;Law and Order&quot;</h2>
<p>Coolidge recorded &quot;Law and Order&quot; for the Nation's Forum series in 1920 as governor of Massachusetts, the year he was elected vice president. It is one of the earliest recordings of his voice:</p>
<blockquote>
<p>&quot;Government is not, must not be, a cold, impersonal machine, but a human and more human agency, appealing to the reason, satisfying the heart, full of mercy, assisting the good, resisting the wrong, delivering the weak from any impositions of the powerful. This is not paternalism, it is not a servitude imposed from without, but the freedom of a righteous self-direction from within.&quot;</p>
</blockquote>
<p><strong>Original (Nation's Forum, 1920)</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/early-presidents-voices-restored/coolidge-1920-original.mp3" aria-label="Original (Nation's Forum, 1920), Calvin Coolidge, 1920: &quot;Law and Order&quot;"><a href="https://diffio.ai/media/blog/early-presidents-voices-restored/coolidge-1920-original.mp3">Original (Nation's Forum, 1920), Calvin Coolidge, 1920: &quot;Law and Order&quot;</a></audio></figure>

<p><strong>Restored with Diffio</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/early-presidents-voices-restored/coolidge-1920-restored.mp3" aria-label="Restored with Diffio, Calvin Coolidge, 1920: &quot;Law and Order&quot;"><a href="https://diffio.ai/media/blog/early-presidents-voices-restored/coolidge-1920-restored.mp3">Restored with Diffio, Calvin Coolidge, 1920: &quot;Law and Order&quot;</a></audio></figure>

<h2 id="franklin-d-roosevelt-1920-americanism">Franklin D. Roosevelt, 1920: &quot;Americanism&quot;</h2>
<p>Twelve years before he was elected president, Roosevelt was the Democratic candidate for vice president. According to the FDR Library, his Nation's Forum record, &quot;Americanism,&quot; gives excerpts from the speech accepting that nomination at Hyde Park on August 9, 1920. It closes on the First World War:</p>
<blockquote>
<p>&quot;The other half is not won yet. The cry of the French at Verdun, 'They shall not pass,' and the cheer of our own men in the Argonne, 'We shall go through,' these were essential glory. Yet they are incomplete. To them we must write the binding finish: it shall not occur again. For America demands that the crime of war shall cease.&quot;</p>
</blockquote>
<p><strong>Original (Nation's Forum, 1920)</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/early-presidents-voices-restored/fdr-1920-original.mp3" aria-label="Original (Nation's Forum, 1920), Franklin D. Roosevelt, 1920: &quot;Americanism&quot;"><a href="https://diffio.ai/media/blog/early-presidents-voices-restored/fdr-1920-original.mp3">Original (Nation's Forum, 1920), Franklin D. Roosevelt, 1920: &quot;Americanism&quot;</a></audio></figure>

<p><strong>Restored with Diffio</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/early-presidents-voices-restored/fdr-1920-restored.mp3" aria-label="Restored with Diffio, Franklin D. Roosevelt, 1920: &quot;Americanism&quot;"><a href="https://diffio.ai/media/blog/early-presidents-voices-restored/fdr-1920-restored.mp3">Restored with Diffio, Franklin D. Roosevelt, 1920: &quot;Americanism&quot;</a></audio></figure>

<h2 id="warren-g-harding-1921-opening-the-washington-naval-conference">Warren G. Harding, 1921: opening the Washington Naval Conference</h2>
<p>On November 12, 1921, the day after he buried the Unknown Soldier at Arlington, Harding opened the Washington Conference on the Limitation of Armament. He recorded the address for Victor in May 1922, on the other side of the disc that carries his Hoboken address over the war dead, and directed that the royalties go to the American Red Cross:</p>
<blockquote>
<p>&quot;Gentlemen of the Conference, the United States welcomes you with unselfish hands. We harbor no fears. We have no sordid ends to serve. We suspect no enemy. We contemplate or apprehend no conquest. Content with what we have, we seek nothing which is another's. We only wish to do with you that finer, nobler thing which no nation can do alone. We wish to sit with you at the table of international understanding and good will.&quot;</p>
</blockquote>
<p><strong>Original (Victor 35718-B, recorded May 24, 1922)</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/early-presidents-voices-restored/harding-1921-arms-original.mp3" aria-label="Original (Victor 35718-B, recorded May 24, 1922), Warren G. Harding, 1921: opening the Washington Naval Conference"><a href="https://diffio.ai/media/blog/early-presidents-voices-restored/harding-1921-arms-original.mp3">Original (Victor 35718-B, recorded May 24, 1922), Warren G. Harding, 1921: opening the Washington Naval Conference</a></audio></figure>

<p><strong>Restored with Diffio</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/early-presidents-voices-restored/harding-1921-arms-restored.mp3" aria-label="Restored with Diffio, Warren G. Harding, 1921: opening the Washington Naval Conference"><a href="https://diffio.ai/media/blog/early-presidents-voices-restored/harding-1921-arms-restored.mp3">Restored with Diffio, Warren G. Harding, 1921: opening the Washington Naval Conference</a></audio></figure>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/early-presidents-voices-restored/victor-35718b-harding-label.webp" alt="Harding's disc, Victor 35718-B. Image: Library of Congress National Jukebox." width="1200" height="1200" loading="lazy" decoding="async"><figcaption>Harding's disc, Victor 35718-B. Image: Library of Congress National Jukebox.</figcaption></figure>

<p>The other side, Harding's 1921 address at Hoboken over 5,212 American war dead, has <a href="https://diffio.ai/blog/harding-1921-hoboken-address-restored/">its own page</a> with the full restored disc.</p>
<h2 id="sources-and-use">Sources and use</h2>
<p>The Taft, Roosevelt, Wilson and Harding discs come from the Library of Congress <a href="https://www.loc.gov/collections/national-jukebox/">National Jukebox</a>, through its Internet Archive copies. The Coolidge and Roosevelt 1920 records come from Internet Archive copies of the Nation's Forum series, which the Library of Congress also holds in its American Leaders Speak collection. Sound recordings published before 1923 entered the public domain in the United States in 2022 under the Music Modernization Act.</p>
<p>Teachers, librarians, museums and writers are welcome to play, embed or link any of these clips. Please credit &quot;Original: [label], [year]; restored with Diffio AI (diffio.ai)&quot;. If you need the full restored discs or WAV files, write to nathan@trydiffio.com.</p>
<p>The same restoration works on recordings much closer to home: a parent's voice on an old cassette, a lecture recorded at the back of a hall, an interview with a noisy room behind it. You can <a href="https://diffio.ai/">try Diffio</a> on your own recordings.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Hear the 1912 election in the candidates&apos; own voices, restored</title>
      <link>https://diffio.ai/blog/1912-election-roosevelt-wilson-taft-voices-restored/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/1912-election-roosevelt-wilson-taft-voices-restored/</guid>
      <pubDate>Sun, 01 Nov 2026 03:31:44 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>Audio restoration</category>
      <category>History</category>
      <description>Theodore Roosevelt, Woodrow Wilson and William Howard Taft recorded their 1912 campaign speeches for Victor. Hear the Library of Congress discs before and after restoration with Diffio AI.</description>
      <media:content url="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/victor-35250-roosevelt-label.webp" medium="image"/>
      <content:encoded><![CDATA[<p>In the fall of 1912 three men who had been or would become president recorded their campaign arguments for the Victor Talking Machine Company. Theodore Roosevelt, running as a Progressive, recorded on September 22. Woodrow Wilson, the Democrat, recorded on September 24. William Howard Taft, the sitting Republican president, recorded on October 1. Victor issued the speeches on disc, so voters could play the candidates' own voices on a phonograph at home.</p>
<p>The discs survive, digitized by the Library of Congress for its National Jukebox. They are acoustic recordings, made without a microphone, and the voices sit under a century of hiss and crackle. For Election Day we ran three of them through Diffio AI, our speech restoration tool. This page puts each original next to its restoration so anyone can hear both. Each pair is the same passage, matched for loudness, so what changes is the noise and the voice, not the volume.</p>
<h2 id="watch-roosevelt-before-and-after">Watch: Roosevelt, before and after</h2>
<figure class="blog-figure blog-figure--video"><video controls playsinline preload="metadata" src="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/roosevelt-1912-before-after.mp4" poster="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/roosevelt-1912-before-after-poster.webp" width="1080" height="1080" aria-label="Watch: Roosevelt, before and after"><a href="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/roosevelt-1912-before-after.mp4">Watch: Roosevelt, before and after</a></video></figure>

<h2 id="theodore-roosevelt-they-dread-you-the-people">Theodore Roosevelt: &quot;They dread you, the people&quot;</h2>
<p>From &quot;Why the Trusts and Bosses Oppose the Progressive Party&quot; (Victor 35250-A). Roosevelt is answering Standard Oil's John D. Archbold and Senator Boies Penrose, who had testified against him that year, and turns their attacks into his case for the Progressive platform:</p>
<blockquote>
<p>&quot;The fundamental motive that induces them to act as they have acted in this manner is not merely that they dislike me, but far more because they dread you. They dread you, the people, you and those like you who make up the people of the United States. They know that their time has come if once the people obtain real power. We stand for the rights of the people. We stand for the rights of the wage worker. We stand for his right to a living wage.&quot;</p>
</blockquote>
<p><strong>Original (Victor 35250-A, recorded September 22, 1912)</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/roosevelt-1912-original.mp3" aria-label="Original (Victor 35250-A, recorded September 22, 1912), Theodore Roosevelt: &quot;They dread you, the people&quot;"><a href="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/roosevelt-1912-original.mp3">Original (Victor 35250-A, recorded September 22, 1912), Theodore Roosevelt: &quot;They dread you, the people&quot;</a></audio></figure>

<p><strong>Restored with Diffio</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/roosevelt-1912-restored.mp3" aria-label="Restored with Diffio, Theodore Roosevelt: &quot;They dread you, the people&quot;"><a href="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/roosevelt-1912-restored.mp3">Restored with Diffio, Theodore Roosevelt: &quot;They dread you, the people&quot;</a></audio></figure>

<h2 id="woodrow-wilson-on-the-third-party">Woodrow Wilson on the third party</h2>
<p>From &quot;Woodrow Wilson on the Third Party&quot; (Victor 35251-A). Wilson argues that Roosevelt's plan to regulate the trusts would make partners of the men it meant to control:</p>
<blockquote>
<p>&quot;Mr. Roosevelt puts forth an admirable platform of what he would like to do for the people. But how is he going to do it? He proposes in his platform not to abolish monopoly but to take it under the legal protection of the government and to regulate it. In other words, to take the very men into partnership who have been making it impossible to carry out these great programs by which all of us wish to help the people.&quot;</p>
</blockquote>
<p><strong>Original (Victor 35251-A, recorded September 24, 1912)</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/wilson-1912-original.mp3" aria-label="Original (Victor 35251-A, recorded September 24, 1912), Woodrow Wilson on the third party"><a href="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/wilson-1912-original.mp3">Original (Victor 35251-A, recorded September 24, 1912), Woodrow Wilson on the third party</a></audio></figure>

<p><strong>Restored with Diffio</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/wilson-1912-restored.mp3" aria-label="Restored with Diffio, Woodrow Wilson on the third party"><a href="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/wilson-1912-restored.mp3">Restored with Diffio, Woodrow Wilson on the third party</a></audio></figure>

<h2 id="william-howard-taft-who-are-the-people">William Howard Taft: &quot;Who are the people?&quot;</h2>
<p>From &quot;Who Are the People&quot; (Victor 35256-A). Taft, the incumbent, answers both challengers' claim to speak for the people:</p>
<blockquote>
<p>&quot;Who are the people? They are not alone the unfortunate and the weak. They are the weak and the strong, the poor and the rich, and the many who are neither. The wage earner and the capitalist, the farmer and the professional man, the merchant and the manufacturer, the storekeeper and the clerk, the railroad manager and the employee. They all make up the people.&quot;</p>
</blockquote>
<p><strong>Original (Victor 35256-A, recorded October 1, 1912)</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/taft-1912-original.mp3" aria-label="Original (Victor 35256-A, recorded October 1, 1912), William Howard Taft: &quot;Who are the people?&quot;"><a href="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/taft-1912-original.mp3">Original (Victor 35256-A, recorded October 1, 1912), William Howard Taft: &quot;Who are the people?&quot;</a></audio></figure>

<p><strong>Restored with Diffio</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/taft-1912-restored.mp3" aria-label="Restored with Diffio, William Howard Taft: &quot;Who are the people?&quot;"><a href="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/taft-1912-restored.mp3">Restored with Diffio, William Howard Taft: &quot;Who are the people?&quot;</a></audio></figure>

<p>Wilson won the election on November 5, 1912. Roosevelt came second, the only third-party candidate ever to do so, and Taft third. More than a century later, their argument about who speaks for &quot;the people&quot; still sounds familiar.</p>
<h2 id="what-changed">What changed</h2>
<p>Diffio restores recorded speech. It separates the voice from everything else in the recording and rebuilds it so it sounds closer to a clean microphone. On these discs the surface noise drops away and the voices come forward.</p>
<p>The models are generative, which is why the result can be so much clearer than a noise filter manages on audio this old. It is also why we publish each original next to its restoration. We treat the restored files as listening copies: the originals stay exactly as the Library of Congress digitized them, and anyone quoting these speeches should check the words against the originals. The quotations on this page are from our own transcription; the full discs are in the <a href="https://www.loc.gov/collections/national-jukebox/">National Jukebox</a>.</p>
<p>The same kind of restoration works on recordings much closer to home: a family cassette, an interview recorded on a phone, a radio program saved on reel-to-reel. If you have one, you can <a href="https://diffio.ai/">try Diffio on three uploads without a card</a>.</p>
<h2 id="for-journalists-educators-and-museums">For journalists, educators and museums</h2>
<p>The original recordings are in the public domain: they were first published in 1912, and U.S. sound recordings published before 1923 entered the public domain in 2022. The restored audio and the video on this page are free to use for press, classroom and museum work. Write to me at nathan.harmon@trydiffio.com if you'd like WAV files or the full restored discs. Please credit &quot;Audio: Victor Talking Machine Co. (1912), Library of Congress National Jukebox; restored with Diffio AI (diffio.ai)&quot;.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/victor-35250-roosevelt-label.webp" alt="Victor record label for Theodore Roosevelt, Why the Trusts and Bosses Oppose the Progressive Party, Victor 35250-A" width="1200" height="1200" loading="lazy" decoding="async"><figcaption>Roosevelt's disc, Victor 35250-A. Image: Library of Congress National Jukebox.</figcaption></figure>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/victor-35251-wilson-label.webp" alt="Victor record label for Woodrow Wilson on the Third Party, Victor 35251-A" width="1200" height="1200" loading="lazy" decoding="async"><figcaption>Wilson's disc, Victor 35251-A. Image: Library of Congress National Jukebox.</figcaption></figure>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/1912-election-roosevelt-wilson-taft-voices-restored/victor-35256-taft-label.webp" alt="Victor record label for William Howard Taft, Who Are the People, Victor 35256-A" width="1200" height="1200" loading="lazy" decoding="async"><figcaption>Taft's disc, Victor 35256-A. Image: Library of Congress National Jukebox.</figcaption></figure>

<p><em>Nathan Harmon is the founder of Diffio AI, based in Colorado Springs.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Hear Warren G. Harding&apos;s 1921 Hoboken address for the WWI dead, restored</title>
      <link>https://diffio.ai/blog/harding-1921-hoboken-address-restored/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/harding-1921-hoboken-address-restored/</guid>
      <pubDate>Sun, 01 Nov 2026 00:23:03 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>Audio restoration</category>
      <category>History</category>
      <description>Warren G. Harding&apos;s 1922 recording of his Hoboken address for 5,212 American war dead, before and after restoration with Diffio AI. Public domain audio, free for press and classroom use.</description>
      <media:content url="https://diffio.ai/media/blog/harding-1921-hoboken-address-restored/victor-35718-harding-label.webp" medium="image"/>
      <content:encoded><![CDATA[<p>On May 23, 1921, President Warren G. Harding stood on Pier 4 in Hoboken, New Jersey, beside the coffins of 5,212 Americans who had died in the First World War and had just been brought home. A year and a day later, on May 24, 1922, he spoke the same address into a recording horn at the White House. Victor released the disc that November, with the proceeds going to the Red Cross.</p>
<p>That disc survives, digitized by the Library of Congress for its National Jukebox. It is an acoustic recording, made without a microphone, and a century of surface noise sits on top of it: a roar of hiss and crackle with Harding's voice somewhere underneath. For Veterans Day we ran it through Diffio AI, our speech restoration tool. This page puts the original and the restoration side by side so anyone can hear both.</p>
<h2 id="listen-the-1922-recording-as-it-survives">Listen: the 1922 recording as it survives</h2>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/harding-1921-hoboken-address-restored/harding-1921-hoboken-original.mp3" aria-label="Listen: the 1922 recording as it survives"><a href="https://diffio.ai/media/blog/harding-1921-hoboken-address-restored/harding-1921-hoboken-original.mp3">Listen: the 1922 recording as it survives</a></audio></figure>

<h2 id="listen-the-same-passage-restored-with-diffio">Listen: the same passage, restored with Diffio</h2>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/harding-1921-hoboken-address-restored/harding-1921-hoboken-restored.mp3" aria-label="Listen: the same passage, restored with Diffio"><a href="https://diffio.ai/media/blog/harding-1921-hoboken-address-restored/harding-1921-hoboken-restored.mp3">Listen: the same passage, restored with Diffio</a></audio></figure>

<p>Both clips are the same 35 seconds, the passage the speech is remembered for, and both are matched for loudness, so what changes is the noise and the voice, not the volume:</p>
<blockquote>
<p>&quot;One's words fail, his understanding is halted, his emotions are stirred beyond control when contemplating these thousands of beloved dead. I find a hundred thousand sorrows touching my heart, and there is ringing in my ears, like an admonition eternal, an insistent call, 'It must not be again. It must not be again.' God grant that it will not be, and let a practical people join in cooperation with God to the end that it shall not be.&quot;</p>
</blockquote>
<h2 id="watch-20-seconds-before-and-after">Watch: 20 seconds, before and after</h2>
<figure class="blog-figure blog-figure--video"><video controls playsinline preload="metadata" src="https://diffio.ai/media/blog/harding-1921-hoboken-address-restored/harding-1921-before-after.mp4" poster="https://diffio.ai/media/blog/harding-1921-hoboken-address-restored/harding-1921-before-after-poster.webp" width="1280" height="720" aria-label="Watch: 20 seconds, before and after"><a href="https://diffio.ai/media/blog/harding-1921-hoboken-address-restored/harding-1921-before-after.mp4">Watch: 20 seconds, before and after</a></video></figure>

<h2 id="the-full-address">The full address</h2>
<p>The whole 4 minute 25 second disc, before and after:</p>
<p><strong>Original (Victor 35718-A, 1922)</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/harding-1921-hoboken-address-restored/harding-1921-hoboken-original-full.mp3" aria-label="Original (Victor 35718-A, 1922), The full address"><a href="https://diffio.ai/media/blog/harding-1921-hoboken-address-restored/harding-1921-hoboken-original-full.mp3">Original (Victor 35718-A, 1922), The full address</a></audio></figure>

<p><strong>Restored with Diffio</strong></p>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/harding-1921-hoboken-address-restored/harding-1921-hoboken-restored-full.mp3" aria-label="Restored with Diffio, The full address"><a href="https://diffio.ai/media/blog/harding-1921-hoboken-address-restored/harding-1921-hoboken-restored-full.mp3">Restored with Diffio, The full address</a></audio></figure>

<p>The 1922 recording is a little shorter than the speech Harding gave in Hoboken. A short passage near the end (&quot;They gave all which men and women can give...&quot;) is not on the disc; the Library of Congress <a href="https://blogs.loc.gov/now-see-hear/2022/05/100-years-ago-president-warren-g-harding-recreates-his-1921-world-war-i-memorial-speech/">prints the full text with the omitted lines marked</a>.</p>
<p><strong>Correction (November 3, 2026):</strong> checking the full restoration word by word against the original disc, we found one passage the model got wrong. About 3 minutes 27 seconds in, Harding says &quot;of a nation being so righteous as never to&quot;; the words are clear on the original, but in the restored file they come out garbled, closer to &quot;even&quot;. This is the kind of error a generative model can make, and it is why the original stays the reference: quote Harding from the original, not the restoration. The 35-second excerpt above does not include this passage, and we checked it against the published text.</p>
<h2 id="what-changed">What changed</h2>
<p>Diffio restores recorded speech. It separates the voice from everything else in the recording and rebuilds it so it sounds closer to a clean microphone. On this disc the surface noise drops away and Harding's voice, which on the original sits underneath it, comes forward.</p>
<p>The models are generative, which is why the result can be so much clearer than a noise filter manages on audio this old. It is also why we publish the original next to the restoration. We treat the restored file as a listening copy: the original stays exactly as the Library of Congress digitized it, and anyone quoting Harding should check the words against it and against the published text.</p>
<h2 id="six-months-later-the-unknown-soldier">Six months later: the Unknown Soldier</h2>
<p>On November 11, 1921, the third anniversary of the armistice, Harding presided at the burial of the Unknown Soldier at Arlington National Cemetery. This Veterans Day marks 105 years of the Tomb. The Hoboken address is the closest thing we have to hearing him speak about the war dead in his own voice, and the line at its center, &quot;It must not be again,&quot; is one that every generation since has had reason to repeat.</p>
<p>The same kind of restoration works on recordings much closer to home: a grandfather's cassette about his service, a reel-to-reel of a Veterans Day service, an oral history taped in a noisy hall. If you have one, you can <a href="https://diffio.ai/">try Diffio on three uploads without a card</a>.</p>
<h2 id="for-journalists-educators-and-museums">For journalists, educators and museums</h2>
<p>The original recording is in the public domain: it was first published in 1922, and U.S. sound recordings published before 1923 entered the public domain in 2022. The restored audio and the 20-second video on this page are free to use for press, classroom and museum work. Write to me at nathan.harmon@trydiffio.com if you'd like WAV files. Please credit &quot;Audio: Warren G. Harding, Victor 35718-A (1922), Library of Congress National Jukebox; restored with Diffio AI (diffio.ai)&quot;.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/harding-1921-hoboken-address-restored/victor-35718-harding-label.webp" alt="Victor record label for Warren G. Harding's address at Hoboken, Victor 35718-A" width="1200" height="1200" loading="lazy" decoding="async"><figcaption>The Victor label for Harding's recording. Image: Library of Congress National Jukebox.</figcaption></figure>

<p><em>Nathan Harmon is the founder of Diffio AI, based in Colorado Springs.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Hear Amelia Earhart&apos;s 1935 broadcast, restored</title>
      <link>https://diffio.ai/blog/amelia-earhart-1935-broadcast-restored/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/amelia-earhart-1935-broadcast-restored/</guid>
      <pubDate>Sat, 31 Oct 2026 22:02:23 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>Audio restoration</category>
      <category>History</category>
      <description>The original 1935 radio broadcast and the Diffio restoration side by side, with press and classroom use details.</description>
      <media:content url="https://diffio.ai/media/blog/amelia-earhart-1935-broadcast-restored/photo_amelia-earhart-electra-1937_1400x1020.webp" medium="image"/>
      <content:encoded><![CDATA[<p>In 1935, two years before she disappeared over the Pacific, Amelia Earhart spoke on the radio about what science and technology had done for women. The talk, known as &quot;A Woman's Place in Science&quot;, survives as a recording, and like most radio audio from the 1930s it is hard work to listen to: a steady bed of hiss, crackle on top of it, and a voice that sounds as if it is coming through a wall.</p>
<p>We ran the broadcast through Diffio AI, our speech restoration tool, and published the result on the diffio.ai homepage. The Daily Mail <a href="https://www.dailymail.com/sciencetech/article-16214191/Hear-Amelia-Earharts-VOICE-AI-recording.html">covered it on October 24, 2026</a>. This page puts the original and the restored version side by side, so anyone can hear both and judge for themselves.</p>
<h2 id="listen-the-original-1935-broadcast">Listen: the original 1935 broadcast</h2>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/amelia-earhart-1935-broadcast-restored/earhart-1935-noisy.mp3" aria-label="Listen: the original 1935 broadcast"><a href="https://diffio.ai/media/blog/amelia-earhart-1935-broadcast-restored/earhart-1935-noisy.mp3">Listen: the original 1935 broadcast</a></audio></figure>

<h2 id="listen-the-same-passage-restored-with-diffio">Listen: the same passage, restored with Diffio</h2>
<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/amelia-earhart-1935-broadcast-restored/earhart-1935-clean.mp3" aria-label="Listen: the same passage, restored with Diffio"><a href="https://diffio.ai/media/blog/amelia-earhart-1935-broadcast-restored/earhart-1935-clean.mp3">Listen: the same passage, restored with Diffio</a></audio></figure>

<p>Both clips are the same 32 seconds. The restored one is louder because the noise that sat under the voice is gone; turn your volume down a little before you switch.</p>
<h2 id="what-changed">What changed</h2>
<p>Diffio restores recorded speech. It separates the voice from everything else in the recording and rebuilds the voice so it sounds closer to a clean microphone: background noise and hiss are removed, room echo is reduced, and supported clipping and distortion are repaired. On this broadcast that means the hiss and crackle drop away and Earhart's voice comes forward.</p>
<p>The models are generative, which is why the result can sound so much clearer than older noise filters manage on audio this old. It is also why we publish the original next to the restoration. For historical recordings we treat the restored file as a listening copy: the original stays exactly as it was, and anyone quoting Earhart should check the words against it.</p>
<h2 id="why-old-voices-matter">Why old voices matter</h2>
<p>&quot;History feels different when you can hear someone speak,&quot; I told the Daily Mail. &quot;With Amelia Earhart, it's a chance to hear the person behind the photographs and the extraordinary story.&quot; Most people know her from black-and-white pictures and from the search for her Electra, which went back to Nikumaroro this October. Far fewer have heard her talk, partly because the recordings that survive are so rough. Making them easier to listen to is a small way to bring her a little closer.</p>
<p>The same applies to recordings in family closets, church archives and local history collections: cassette interviews with grandparents, reel-to-reel sermons, oral histories taped in noisy rooms. If you have one of those, you can <a href="https://diffio.ai/">try Diffio on three uploads without a card</a>.</p>
<h2 id="for-journalists-and-educators">For journalists and educators</h2>
<p>The original and restored audio of this broadcast are cleared for press, classroom and museum use, along with a 10-second before-and-after video. Write to me at nathan.harmon@trydiffio.com and I'll send the files. Please credit &quot;Audio: Amelia Earhart, 1935 radio broadcast, restored with Diffio AI (diffio.ai)&quot;.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/amelia-earhart-1935-broadcast-restored/photo_amelia-earhart-electra-1937_1400x1020.webp" alt="Amelia Earhart standing under the nose of her Lockheed Electra, 1937" width="1400" height="1020" loading="lazy" decoding="async"><figcaption>Amelia Earhart with her Lockheed Model 10-E Electra, March 1937. Photo: Underwood &amp; Underwood, 1937, Smithsonian National Air and Space Museum, public domain.</figcaption></figure>

<p><em>Nathan Harmon is the founder of Diffio AI, based in Colorado Springs.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Overlapping I/O With Inference Around One Shared Model, Not Batching, Raised Mac Mini Throughput on I/O-Bound Restores</title>
      <link>https://diffio.ai/blog/mac-mini-inference-serving/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/mac-mini-inference-serving/</guid>
      <pubDate>Wed, 07 Oct 2026 16:33:19 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>ML</category>
      <category>Inference</category>
      <category>Apple Silicon</category>
      <description>How Diffio raised Mac mini throughput on I/O-bound speech restores by overlapping downloads, encoding and uploads with inference around one shared model, rather than batching.</description>
      <content:encoded><![CDATA[<p>Diffio 4.5 <a href="https://diffio.ai/audio-cleaner/">restores speech</a> on a fleet of Mac minis (M4 Pro, 24 GB of unified memory). Each mini runs one worker, which can serve our production environment, our dev environment or both. The worker takes jobs from each enabled environment's edge scheduler (a Cloudflare Durable Object that queues jobs and hands them to connected workers), downloads the upload and its transcript, restores it, and publishes the result as HLS (HTTP Live Streaming) audio that plays while later parts are still restoring, then as final files. The worker used to run one job at a time, so under load users waited for whole jobs ahead of them. It now runs several jobs at once around a single copy of the model, and a burst of six uploads drains 3.3x faster.</p>
<div class="blog-table" role="region" aria-label="Table 1" tabindex="0"><table>
<thead>
<tr>
<th>Six 45 s jobs submitted at once, one mini, stems uploaded one at a time</th>
<th>Makespan</th>
<th>Median time to first audio</th>
<th>Worst time to first audio</th>
</tr>
</thead>
<tbody>
<tr>
<td>One job at a time</td>
<td>140 s</td>
<td>63 s</td>
<td>120 s</td>
</tr>
<tr>
<td>Three jobs at once, one model</td>
<td>59 s</td>
<td>16 s</td>
<td>41 s</td>
</tr>
<tr>
<td>Six jobs at once, one model</td>
<td>42 s</td>
<td>6.1 s</td>
<td>9.8 s</td>
</tr>
</tbody>
</table>
</div><p>The makespan runs from submitting the burst to the last job's completion; time to first audio runs from submitting a job until its first HLS segment is playable.</p>
<h2 id="inference-occupied-7-26-of-a-dev-restore-jobs-wall-clock-time">Inference occupied 7–26% of a dev restore job's wall-clock time</h2>
<p>Across logged dev restore jobs, inference took 15% of wall-clock time; the rest was downloading, decoding, MP3 and HLS encoding, and uploading through the edge. The fraction depends on the sampler: pro, which uses best-of-three sampling and selects the speech and the background separately, spends longer in the model than flash, the default.</p>
<div class="blog-table" role="region" aria-label="Table 2" tabindex="0"><table>
<thead>
<tr>
<th>Audio, sampler (12 dev jobs)</th>
<th>Wall-clock job time</th>
<th>Inference time</th>
<th>Inference fraction</th>
</tr>
</thead>
<tbody>
<tr>
<td>11–45 s, flash</td>
<td>7.5–23 s</td>
<td>0.8–1.7 s</td>
<td>7–11%</td>
</tr>
<tr>
<td>11–30 s, pro</td>
<td>9.9–21 s</td>
<td>2.6–4.4 s</td>
<td>21–26%</td>
</tr>
<tr>
<td>3 min, pro</td>
<td>102 s</td>
<td>18 s</td>
<td>18%</td>
</tr>
</tbody>
</table>
</div><p>With inference as the serial part, overlapping jobs can raise throughput by at most the inverse of the inference fraction (Amdahl's law), 9–14x for flash and 4–5x for pro, and by at most the number of jobs allowed at once; both bounds assume the I/O and encoding of different jobs overlap without contending. Once stems moved after job completion (described later), inference became about 70% of a production pro job, which caps the overlap gain for those jobs near 1.4x; the burst speedup applies to I/O-bound jobs.</p>
<h2 id="a-single-inference-thread-serves-several-concurrent-job-slots">A single inference thread serves several concurrent job slots</h2>
<p>The worker now runs up to six jobs at once. Each job does its downloads, decoding, encoding and uploads on its own thread; restore jobs queue for a single inference thread, the one thread that loads and runs the model, so MLX and PyTorch MPS state stays on one thread as it did when one job thread did everything. The inference queue serves production restores before dev ones, and within each environment shortest job first by expected inference time: audio seconds divided by the sampler's measured speed, 11x real time for pro and 35x for flash. Shortest job first can starve a long file, so the queue applies aging: a restore that has waited a bounded time or been passed over a bounded number of times goes next within its environment. A job waiting for inference stays in the &quot;preparing audio&quot; phase it was already reporting, and a cancel while it waits removes it before it starts.</p>
<p>The scheduler had assumed one job per worker, and its protocol changed with the worker. A worker tells each environment's scheduler how many more jobs it can take and which jobs it still runs there (a <code>ready</code> message with <code>slots</code> and <code>jobIds</code>). The scheduler assigns up to that many, re-queues any accepted job the worker no longer lists, and treats a rejection as zero open slots until the next <code>ready</code>. A freed slot is offered to production at once and to dev half a second later; if both environments assign the last slot at the same moment, the worker rejects one and the scheduler re-queues it without counting it against the job's retry limit.</p>
<p>Overlapping jobs makes each one slower, 21–28 s when it runs by itself and 33–40 s among six, because their uploads share the mini's uplink. The burst's makespan is set by the slowest job's I/O, not by inference: inference-thread utilization was 22% over the burst, and a restore took as long with other jobs running as without.</p>
<div class="blog-table" role="region" aria-label="Table 3" tabindex="0"><table>
<thead>
<tr>
<th>Six 3 min jobs at once, six slots</th>
<th>Makespan</th>
<th>Median time to first audio</th>
<th>Worst time to first audio</th>
<th>Worker resident memory</th>
</tr>
</thead>
<tbody>
<tr>
<td>Flash</td>
<td>194 s</td>
<td>17 s</td>
<td>31 s</td>
<td>5.3 GB idle, 7.4 GB peak (about 350 MB per running job), no swap</td>
</tr>
</tbody>
</table>
</div><p>Inference is scheduled non-preemptively, one whole restore at a time, so a long file causes head-of-line blocking: a 20-minute file holds the inference thread for about 33 s with flash and 107 s with pro. Restores already advance window by window, so scheduling at window granularity is the next step; it needs a separate codec decoder KV cache per job, since the decoder keeps its keys and values across windows. Six 20-minute files at once are unmeasured.</p>
<h2 id="stem-uploads-were-bound-by-per-request-cost-not-bandwidth-or-cpu-and-now-follow-the-jobs-completion">Stem uploads were bound by per-request cost, not bandwidth or CPU, and now follow the job's completion</h2>
<p>A restore publishes its separated speech and background as stems; when we measured, that was about 100 WAV and FLAC objects (250 MB) for a 3 min file, each a PUT through the edge. Uploaded serially, they took most of the job, while the worker and ffmpeg used 15 s of CPU. Each PUT costs 0.3–0.5 s of fixed overhead plus its transfer at 5–10 MB/s per stream. The overhead is in the edge, not the 3–22 ms network round trip: the edge checks the job's scheduler lease and writes to R2, Cloudflare's object store where the media lives, on every PUT. Every worker upload pays it, so caching the lease check per job is the next speedup for all of them.</p>
<p>The worker now uploads eight objects at a time while later chunks are still encoding, which overlaps both the overhead and the per-stream transfer. At most 32 wait in the queue, so disk use stays bounded, and the manifest goes last, once every object it lists exists.</p>
<div class="blog-table" role="region" aria-label="Table 4" tabindex="0"><table>
<thead>
<tr>
<th>3 min file, flash, one mini</th>
<th>Stem upload phase, median</th>
<th>Job, median</th>
</tr>
</thead>
<tbody>
<tr>
<td>One at a time</td>
<td>88 s</td>
<td>95 s</td>
</tr>
<tr>
<td>8 at a time</td>
<td>32 s</td>
<td>39 s</td>
</tr>
<tr>
<td>16 at a time</td>
<td>37 s</td>
<td>43 s</td>
</tr>
</tbody>
</table>
</div><p>Eight at a time per job multiplied across six jobs, all contending for one home uplink. Each job still sends at most eight, and a worker-wide AIMD (additive increase, multiplicative decrease) concurrency limit, as in TCP congestion control, now bounds them together: it adds about one request per window without failures and halves on a timeout or failure, within 2–16, and like TCP it treats every failure as congestion; its effect on throughput is unmeasured. Playback outputs have priority over stems for a free request.</p>
<p>Since October 8 stems upload asynchronously, after job completion. The worker reports <code>job.complete</code> once the restored audio and its playback outputs are published, then uploads the stems (FLAC chunks plus speech and background masters, the original now rendered from the upload) within a 45-minute window and reports them in a separate <code>job.stems</code> message. A stem failure never fails the restore. The table covers one <a href="https://diffio.ai/api-overview/">internal API client</a>'s 60 s files on the wired mini.</p>
<div class="blog-table" role="region" aria-label="Table 5" tabindex="0"><table>
<thead>
<tr>
<th>60 s file, pro, production, wired mini, 1,627 jobs</th>
<th>Median</th>
</tr>
</thead>
<tbody>
<tr>
<td>Download and decode</td>
<td>0.45 s</td>
</tr>
<tr>
<td>Inference</td>
<td>5.2 s</td>
</tr>
<tr>
<td>Encode</td>
<td>1.3 s</td>
</tr>
<tr>
<td>Upload</td>
<td>0.49 s</td>
</tr>
<tr>
<td>Job (<code>job.complete</code>)</td>
<td>7.5 s</td>
</tr>
<tr>
<td>Stems published after <code>job.complete</code></td>
<td>1.5–2.5 s</td>
</tr>
</tbody>
</table>
</div><h2 id="the-scheduler-routes-each-restore-to-the-mini-where-its-inference-would-start-first-which-ignores-the-uplink">The scheduler routes each restore to the mini where its inference would start first, which ignores the uplink</h2>
<p>Open slots alone do not say where a restore will start soonest, because a mini with free slots can still have minutes of pro queued for its one model. Workers also report their inference backlog (the running restore's remaining seconds and the queued restores' expected seconds) in <code>ready</code> and in every <code>ping</code>, and the scheduler sends each restore to the mini where its inference would start first. Starts within 10 s of the soonest count as equal, and worker priority, set per worker by our deploy script, then idle time decide among them. Ties are judged against the soonest start, because equality within a tolerance is not transitive: judged against the running best, estimates of 0, 9 and 18 s all tie in turn and the last wins.</p>
<p>That backlog is the only cost routing sees, and on short files it is the same everywhere. Our two minis reach Cloudflare over different home connections, one at about 50 MB/s upstream with a 3 ms round trip, the other at 6.7 MB/s and 22 ms over Wi-Fi. Once we gave them equal priority on October 14, idle models tied and the longest-idle rule sent every other job of a sequential client to the slower one.</p>
<div class="blog-table" role="region" aria-label="Table 6" tabindex="0"><table>
<thead>
<tr>
<th>60 s file, pro, production</th>
<th>Faster uplink</th>
<th>Slower uplink</th>
</tr>
</thead>
<tbody>
<tr>
<td>Job, median (p90)</td>
<td>7.5 s (8.3 s)</td>
<td>8.3 s (12.7 s)</td>
</tr>
<tr>
<td>Stems published after <code>job.complete</code>, median by hour</td>
<td>1.5–2.5 s</td>
<td>5.5–8.2 s</td>
</tr>
</tbody>
</table>
</div><p>Inference took 5.2 s on both. The faster mini's figures cover two days, the slower mini's 70 jobs since the priority change. Moving the slower mini from Wi-Fi to Ethernet did not raise its uplink, 4.5 MB/s wired, because its connection is the limit.</p>
<h2 id="in-order-callbacks-blocked-completion-behind-stale-progress-and-unretried-worker-messages-re-ran-jobs">In-order callbacks blocked completion behind stale progress, and unretried worker messages re-ran jobs</h2>
<p>The scheduler forwards job events to our backend (as <code>job.queued</code>, <code>job.progress</code>, <code>job.completed</code> and so on, the last sent when a worker reports <code>job.complete</code>) through an outbox, a queue in the scheduler's storage that retries each event until it is acknowledged and delivers one job's events in order. Progress within a phase is coalesced, but phase changes and the start and end of each transfer go at once, so even a short restore sent 11 progress events, and its <code>job.completed</code> suffered 0.3–0.7 s of head-of-line blocking behind the undelivered ones. Completion now drops a job's undelivered progress events: its event carries every output they did, and only that job's last intermediate phase and its first-audio time can be lost.</p>
<p>The worker's link to the scheduler had the opposite problem, no retry at all. A message written just before the scheduler dropped a socket was lost, and a worker that reconnected without reporting its completion had the job re-queued and run again: one 95 s job finished after 196 s. Workers now replay their recent terminal job messages after each reconnect, and the scheduler deduplicates them: at-least-once delivery with idempotent handling.</p>
<h2 id="outside-the-fleet-transcription-and-backend-calls-take-longer-than-the-restore">Outside the fleet, transcription and backend calls take longer than the restore</h2>
<p>A 4.5 restore is conditioned on the transcript, so for an API client the fleet job sits between a transcription and the calls that record the result. The client creates a project, uploads the file, then requests a generation (one restore of that project); our backend, Cloud Functions on Google Cloud, runs the transcription, queues the fleet job and marks the generation complete. One traced 60 s pro job:</p>
<div class="blog-table" role="region" aria-label="Table 7" tabindex="0"><table>
<thead>
<tr>
<th>Stage, one 60 s pro API job</th>
<th>Time</th>
</tr>
</thead>
<tbody>
<tr>
<td>Upload, create project</td>
<td>1.8 s</td>
</tr>
<tr>
<td>Create generation</td>
<td>2.0 s</td>
</tr>
<tr>
<td>Generation created to fleet job queued (transcription)</td>
<td>4.0 s</td>
</tr>
<tr>
<td>Fleet job</td>
<td>7.3 s</td>
</tr>
<tr>
<td>Fleet job done to client sees complete (2 s polling)</td>
<td>3.2 s</td>
</tr>
</tbody>
</table>
</div><p>Across a day of our internal client's 60 s pro files, transcriptions took 5.0 s at the median and 11.8 s at the p90, so for short files the serial stages around the fleet cost more than the fleet job.</p>
<h2 id="botocores-default-60-s-read-timeout-turned-unanswered-r2-requests-into-62-s-stalls">botocore's default 60 s read timeout turned unanswered R2 requests into 62 s stalls</h2>
<p>Our backend reaches R2 through boto3: upload checks and probes, the object checks that complete a generation, and transcription artifacts. The client configured retries but no timeouts, so botocore's 60 s default read timeout applied. A request that got no answer waited the full read timeout, then its retry on another connection succeeded, so it ended normally after about 62 s. Cutting the read timeout to 10 s cut the stalls to 11–13 s, which confirms the timeout set their length.</p>
<div class="blog-table" role="region" aria-label="Table 8" tabindex="0"><table>
<thead>
<tr>
<th>Over 12 hours before the fix</th>
<th>Took 15 s or more, most about 62 s</th>
</tr>
</thead>
<tbody>
<tr>
<td>Upload completions</td>
<td>22 of 978</td>
</tr>
<tr>
<td>Generation completion checks</td>
<td>26 of about 950, in pairs</td>
</tr>
</tbody>
</table>
</div><p>The pairs came from our own design. The SDKs report an upload with <code>complete_project_upload</code> as soon as it lands, and the edge sent its own completion callback at the same moment, so two requests for the same upload, like two completion checks for the same generation, called R2 together and stalled together. The edge's callback now waits 30 s as a backup, by which time the upload is normally recorded and the callback returns at once. Stalls also hit our dev and production backends in the same seconds, although the two share no database, only R2 and the network path to it, which points outside both. A dead pooled connection is the leading explanation; stalls were more frequent after an instance had idled 1–5 minutes (4 of 18 checks, against 11 of 258), though the counts are small. Firestore lock contention is ruled out by the code: the transaction that records an upload makes no R2 call, and the R2 check runs before it.</p>
<p>The stalls became failures where a step had a fixed deadline. The transcription steps that write artifacts get about 60 s, so a stalled write failed the transcription, and because the restore needs the transcript, the generation failed with it: on October 14, all 20 failed generations of our internal client were transcription timeouts.</p>
<p>The client now also sets a 5 s <code>connect_timeout</code>, keeps a longer read timeout only for server-side copies and multipart completion, whose work grows with object size, and ffprobe reading an R2 URL uses a 5 s <code>rw_timeout</code>. We also turned on TCP keepalive, which does nothing here: the kernel's default idle time before the first probe is 2 hours. In the hours since, no transcription has failed. Every retried or slow R2 call is now logged with its error, which separates connect failures from reads that got no answer.</p>
<h2 id="scoreq-quality-scoring-runs-on-a-low-priority-cpu-thread-and-adds-no-job-time">ScoreQ quality scoring runs on a low-priority CPU thread and adds no job time</h2>
<p>ScoreQ, a no-reference speech quality model that predicts a mean opinion score (MOS), scores the middle 30 s of each restore's original and restored audio. It runs in onnxruntime on the CPU, from one low-priority thread per worker, off the inference thread and away from the GPU and the Neural Engine. It takes about 2.3 s per job, and runs with scoring on and off showed no difference in job time. <code>job.complete</code> is unchanged; a separate <code>job.quality</code> message follows, sent only to a scheduler whose <code>welcome</code> message lists <code>job.quality</code> among its capabilities, because an older scheduler rejects unknown message types.</p>
<h2 id="a-single-guidance-branch-is-already-compute-bound-so-batching-across-users-adds-at-most-13-throughput">A single guidance branch is already compute-bound, so batching across users adds at most 13% throughput</h2>
<p>Every 4.5 sampler step evaluates three <a href="https://diffio.ai/blog/classifier-free-guidance/">classifier-free guidance</a> branches; the GPU (through MLX) runs the branches that read the transcript, and the Neural Engine runs the rest, the split <a href="https://diffio.ai/blog/diffio-apple-silicon/">the 4.0 port</a> introduced, without its CPU share.</p>
<div class="blog-table" role="region" aria-label="Table 9" tabindex="0"><table>
<thead>
<tr>
<th>4.5 diffusion transformer</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Blocks, width, SwiGLU MLP width</td>
<td>16, 1152, 3072</td>
</tr>
<tr>
<td>Sequence length per guidance branch (mode token, 50 context latents from the previous window, 375 latent frames)</td>
<td>426 tokens</td>
</tr>
<tr>
<td>Compute per branch forward pass</td>
<td>about 272 GFLOP</td>
</tr>
<tr>
<td>M4 Pro memory bandwidth</td>
<td>273 GB/s</td>
</tr>
</tbody>
</table>
</div><p>One branch's forward pass already runs at 4.6 TFLOPS on the GPU.</p>
<div class="blog-table" role="region" aria-label="Table 10" tabindex="0"><table>
<thead>
<tr>
<th>Batch size (guidance branches per GPU forward pass, all reading the transcript)</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>8</th>
<th>16</th>
</tr>
</thead>
<tbody>
<tr>
<td>Time per branch</td>
<td>59.5 ms</td>
<td>56.3 ms</td>
<td>53.9 ms</td>
<td>53.8 ms</td>
<td>53.1 ms</td>
<td>52.9 ms</td>
</tr>
</tbody>
</table>
</div><p>A linear fit to the sweep (5.4 ms per pass plus 52.4 ms per branch) bounds the gain from batching at 13%. Reading the weights once per pass takes about 2 ms, so weight reuse, the usual reason to batch, is not what batching would buy here. Peak memory stays under 3 GB at batch size 16; batching across users is blocked by the implementation instead. Branches from different users have different windows and usually different noise levels, while the forward pass shares one window's condition projections, transcript keys and values and context, and one noise level's adaLN modulation, across its batch; and the Neural Engine's fixed-shape Core ML graphs run one branch at a time.</p>
<h2 id="a-second-copy-of-the-model-time-slices-the-same-gpu-and-costs-8-1-gb">A second copy of the model time-slices the same GPU and costs 8.1 GB</h2>
<p>Two copies of the model each restoring a file took twice as long per file as one copy alone, in all three cases we ran: the second copy gets no hardware the first was not using. As a serving design, two worker copies would each overlap I/O with inference the way job slots do, so they would raise throughput only as much as slots do, and a third copy does not fit in memory.</p>
<div class="blog-table" role="region" aria-label="Table 11" tabindex="0"><table>
<thead>
<tr>
<th>Restores running at once</th>
<th>45 s file, flash</th>
<th>3 min file, flash</th>
<th>45 s file, pro</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>1.5–1.7 s</td>
<td>5.4 s</td>
<td>4.6 s</td>
</tr>
<tr>
<td>2 (each)</td>
<td>3.0–3.3 s</td>
<td>11.5 s</td>
<td>8.7–8.9 s</td>
</tr>
<tr>
<td>3 (each)</td>
<td>7–26 s, swapping</td>
<td></td>
<td></td>
</tr>
<tr>
<td>4</td>
<td>Metal out-of-memory error</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div><div class="blog-table" role="region" aria-label="Table 12" tabindex="0"><table>
<thead>
<tr>
<th>Process of one warm model</th>
<th>Footprint</th>
</tr>
</thead>
<tbody>
<tr>
<td>Main process: MLX transformer, PyTorch MPS for the first window's speech encoder (Metal memory 5.8 GB)</td>
<td>6.4 GB</td>
</tr>
<tr>
<td>CPU process: the next window's first six Granite speech-encoder layers and transcript encoder</td>
<td>1.2 GB</td>
</tr>
<tr>
<td>Neural Engine process: Core ML guidance branches and the codec</td>
<td>0.5 GB</td>
</tr>
<tr>
<td>Total</td>
<td>8.1 GB</td>
</tr>
</tbody>
</table>
</div><p>4.5 no longer runs part of each MLP on the CPU, which freed the CPU for the speech-encoder work. <a href="https://diffio.ai/blog/diffio-apple-silicon/">The 4.0 port</a> reported 10–15x slowdowns with two processes, when its main process alone held 14 GB.</p>
<h2 id="the-default-open-file-limit-is-256-so-a-job-must-close-every-file-descriptor-it-opens">The default open-file limit is 256, so a job must close every file descriptor it opens</h2>
<p>A process started by launchd or an SSH session on macOS gets a soft limit of 256 open files (<code>launchctl limit maxfiles</code> prints <code>256 unlimited</code>). An idle worker holds 37 descriptors, mostly shared-memory IPC segments and pipes to its helper processes, and a running job adds about 25: TCP sockets for its parallel HLS and file uploads, plus ffmpeg pipes and segment files. The default therefore leaves room for about eight jobs.</p>
<div class="blog-table" role="region" aria-label="Table 13" tabindex="0"><table>
<thead>
<tr>
<th>File descriptors in the worker process</th>
<th>Count</th>
</tr>
</thead>
<tbody>
<tr>
<td>Idle</td>
<td>37</td>
</tr>
<tr>
<td>Peak, six 45 s jobs on a fresh worker</td>
<td>184 (134 sockets)</td>
</tr>
<tr>
<td>Peak, six 3 min jobs on a fresh worker</td>
<td>147</td>
</tr>
<tr>
<td>After one burst of six jobs, before the leak fixes</td>
<td>187</td>
</tr>
<tr>
<td>After every burst, with the fixes</td>
<td>37</td>
</tr>
</tbody>
</table>
</div><p>Two leaks turned that margin into failures. Each job's HTTP client kept its connection pool open after the job ended, and each of its two HLS ffmpeg encoders kept a stderr pipe open in the worker. In the burst after one that left 187 descriptors open, the worker crossed 256: a job failed with <code>OSError: [Errno 24] Too many open files</code>, and uploads failed with <code>nodename nor servname provided</code>, a name-resolution error that disappeared with the leaks. Each job now closes its connection pool, and ffmpeg's errors go to a file instead of a pipe.</p>
<p>The worker also raises its soft limit at startup with <code>setrlimit(RLIMIT_NOFILE)</code>, which ffmpeg and the helper processes inherit. macOS rejects an unlimited soft limit, and setrlimit(2) recommends <code>min(OPEN_MAX, hard limit)</code>, which is 10240 here. Doing it in code covers the service and the same code started from a shell for smoke tests and benchmarks; a <code>NumberOfFiles</code> entry in a LaunchDaemon's <code>SoftResourceLimits</code> would also set the system-wide <code>kern.maxfiles</code>.</p>
<h2 id="long-jobs-and-days-of-uptime-not-bursts-grew-the-worker-past-24-gb">Long jobs and days of uptime, not bursts, grew the worker past 24 GB</h2>
<p>The burst peak above is from a fresh worker. In service, one 60-minute restore pushed the worker past physical memory into 8.6 GB of swap, and an idle worker held 11 GB after a day of jobs. Three things held memory that no burst of short jobs exposes:</p>
<ul>
<li>The inference thread kept a reference to the last restore it finished, with its full-length arrays, until the next restore replaced it. It now releases the finished job.</li>
<li>ScoreQ's onnxruntime session kept a CPU memory arena at its high-water mark, 2.5 GB after a few jobs and growing. The arena is now off.</li>
<li>Each job's full-length audio timelines lived in anonymous memory, which under pressure macOS can only compress or swap. They are now shared memory-mapped files in the job's folder, whose dirty pages the kernel writes back to their own file and drops; they are released when the job unmaps them and deletes the folder. The model's MLX and Metal memory and short-lived arrays stay anonymous.</li>
</ul>
<p>Outputs are byte-identical. A 60-minute restore has not been re-measured since, and an unidentified allocation still accumulates: after several days of production uptime, one mini held 2.3 GB of swap, and redeploying the worker brought that to 0.5 GB.</p>
<h2 id="the-worker-sweeps-its-work-and-temporary-directories-at-startup-alerts-below-20-gb-free-without-refusing-jobs-and-deploys-keep-two-releases">The worker sweeps its work and temporary directories at startup, alerts below 20 GB free without refusing jobs, and deploys keep two releases</h2>
<p>Each job writes into its own folder under the worker's work directory, removed when the job completes or fails. A crash or power loss mid-job leaves that folder behind, so the worker empties its work directory when it starts, when nothing can be running. Temporary files escaped that rule: one mini's per-user temporary directory held 14 GB of compiled Core ML models (<code>.mlmodelc</code> directories) left by model builds and research runs.</p>
<p>Core ML compiles a model package into a temporary location. Our loader copies each compiled model next to its package, so compiling happens once per machine, and it now deletes the temporary copy as soon as it has its own. The worker also points <code>TMPDIR</code> at a folder inside its work directory, for itself and every process it starts, so the startup sweep covers Python's and ffmpeg's temporary files.</p>
<p>A release's code is 6.6 MB, but the weights bundle (the checkpoints, the speech-encoder and codec models, and the converted Core ML and MLX copies) is 24 GB, so a deploy keeps only the new release and the previous one, the rollback target, and deletes weight bundles neither uses. Seeding a bundle from a folder already on the mini uses APFS copy-on-write clones (<code>cp -c</code>), which share blocks until a file changes.</p>
<p>Below 20 GB free on the work volume (<code>disk_alert_gb</code> in the worker's configuration) the worker reports a warning to our analytics, and below 5 GB an error, repeated while it lasts; a deploy prints the warning and goes ahead. A job that actually runs out of space fails on its own. The worker used to offer no slots below the threshold, which would have taken a mini out of service with 20 GB still free.</p>
<p><code>/Library/Caches/com.apple.aned</code>, where the Neural Engine service keeps the models it has compiled in a folder per macOS build, stays <code>Operation not permitted</code> over SSH even with full disk access and sudo, so <code>du</code> cannot measure it. On one of ours, a minor macOS update that changed the build took used space from about 408 GB to 50 GB, and the worker's first model load afterwards recompiled its Neural Engine graphs into the new build's folder. That cache grows as the Neural Engine compiles new models, so model experiments stay off fleet minis, and the low-disk warning reports a mini before its disk fills.</p>
<h2 id="a-fleet-mini-runs-the-worker-and-nothing-else">A fleet mini runs the worker and nothing else</h2>
<p>One of our minis had also served as a desktop and a research machine: it ran the ChatGPT desktop app, started at login by a launch agent, Chrome, a research restore process left from a previous day, and its research checkouts. It had 4.4 GB of swap in use; after the cleanup, 0.5 GB. Any desktop working set competes directly with the model and the running jobs.</p>
<p>The worker checks every minute for processes of its user outside macOS and its own folder, launch agents and daemons other than its own and Tailscale's, and active swapping, and reports any it finds as a warning in our analytics. Swap occupancy was the first signal and proved too coarse, since macOS leaves cold pages in swap long after the pressure that put them there; the worker now warns on the swap-out rate, more than 256 MB written since its previous check. The deploy script refuses a mini that fails the process and launch-item checks, and checks swap only after the old worker has exited, since the old worker's swap is freed with it; if heavy swap remains, it restarts the previous release.</p>
<p>Desktop software comes back on its own: after a reboot, macOS reopened Chrome from the saved login session, and Chrome reinstalled three Google updater launch agents. <code>defaults write com.apple.loginwindow TALLogoutSavesState -bool false</code> stops apps from reopening at login.</p>
<h2 id="without-a-logged-in-user-macos-27s-audiomxd-and-configd-together-use-more-than-a-cpu-core">Without a logged-in user, macOS 27's audiomxd and configd together use more than a CPU core</h2>
<p>On a mini running macOS 27.0 (26A428) with no console user, <code>audiomxd</code> used 70–74% of a core and <code>configd</code> 45%, from boot onward. A <code>sample</code> of audiomxd shows a tight loop: Bluetooth audio routing (<code>-[BTAudioRoutingRequest updateAudioState:withState:]</code>) tries to start an XPC connection to the console user's session, asks configd for the console user (<code>SCDynamicStoreCopyConsoleUser</code>), finds none, and retries. Its log repeats <code>UpdateAudioState failed to start XPC: kUnexpectedErr (No user logged in)</code>. Bluetooth was on, with nothing paired.</p>
<p>Bluetooth cannot be turned off headlessly on macOS 27: with <code>ControllerPowerState</code> set to 0 in <code>/Library/Preferences/com.apple.Bluetooth</code>, Bluetooth is still on after restarting bluetoothd or rebooting, and <code>blueutil -p 0</code> cannot reach the controller without a GUI session, even as root. launchd restarts a killed audiomxd, which loops again within seconds. Auto-login for the service user provides a console user, and both processes drop to 0% CPU. Our other mini, on macOS 26.7, has always logged in automatically and never showed the loop.</p>
<h2 id="on-macos-27-auto-login-needs-etc-kcpassword-written-by-hand">On macOS 27, auto-login needs /etc/kcpassword written by hand</h2>
<p><code>sudo sysadminctl -autologin set -userName &lt;user&gt; -password -</code> prints <code>SACSetAutoLoginPassword error:22</code> even with the correct password, then exits 0. It writes <code>autoLoginUser</code> but not <code>/etc/kcpassword</code>, so the mini still boots to the login window. Auto-login needs both the <code>autoLoginUser</code> key in <code>/Library/Preferences/com.apple.loginwindow</code> and <code>/etc/kcpassword</code>, which holds the password obfuscated with a fixed XOR key. The script below verifies the password with <code>dscl -authonly</code> and writes both. getpass keeps the password out of shell history; it appears in <code>dscl</code>'s arguments only while dscl runs.</p>
<pre class="blog-code shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D">#!/usr/bin/env python3</span></span>
<span class="line"><span style="color:#032F62">"""Turn on auto-login for USER. Run as: sudo python3 set_autologin.py USER"""</span></span>
<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> getpass, os, subprocess, sys</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5">KEY</span><span style="color:#D73A49"> =</span><span style="color:#005CC5"> bytes</span><span style="color:#24292E">([</span><span style="color:#D73A49">0x</span><span style="color:#005CC5">7D</span><span style="color:#24292E">, </span><span style="color:#D73A49">0x</span><span style="color:#005CC5">89</span><span style="color:#24292E">, </span><span style="color:#D73A49">0x</span><span style="color:#005CC5">52</span><span style="color:#24292E">, </span><span style="color:#D73A49">0x</span><span style="color:#005CC5">23</span><span style="color:#24292E">, </span><span style="color:#D73A49">0x</span><span style="color:#005CC5">D2</span><span style="color:#24292E">, </span><span style="color:#D73A49">0x</span><span style="color:#005CC5">BC</span><span style="color:#24292E">, </span><span style="color:#D73A49">0x</span><span style="color:#005CC5">DD</span><span style="color:#24292E">, </span><span style="color:#D73A49">0x</span><span style="color:#005CC5">EA</span><span style="color:#24292E">, </span><span style="color:#D73A49">0x</span><span style="color:#005CC5">A3</span><span style="color:#24292E">, </span><span style="color:#D73A49">0x</span><span style="color:#005CC5">B9</span><span style="color:#24292E">, </span><span style="color:#D73A49">0x</span><span style="color:#005CC5">1F</span><span style="color:#24292E">])</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E">user </span><span style="color:#D73A49">=</span><span style="color:#24292E"> sys.argv[</span><span style="color:#005CC5">1</span><span style="color:#24292E">]</span></span>
<span class="line"><span style="color:#24292E">password </span><span style="color:#D73A49">=</span><span style="color:#24292E"> getpass.getpass(</span><span style="color:#D73A49">f</span><span style="color:#032F62">"Password for </span><span style="color:#005CC5">{</span><span style="color:#24292E">user</span><span style="color:#005CC5">}</span><span style="color:#032F62">: "</span><span style="color:#24292E">)</span></span>
<span class="line"><span style="color:#D73A49">if</span><span style="color:#24292E"> subprocess.run([</span><span style="color:#032F62">"dscl"</span><span style="color:#24292E">, </span><span style="color:#032F62">"."</span><span style="color:#24292E">, </span><span style="color:#032F62">"-authonly"</span><span style="color:#24292E">, user, password]).returncode </span><span style="color:#D73A49">!=</span><span style="color:#005CC5"> 0</span><span style="color:#24292E">:</span></span>
<span class="line"><span style="color:#24292E">    sys.exit(</span><span style="color:#032F62">"wrong password"</span><span style="color:#24292E">)</span></span>
<span class="line"><span style="color:#24292E">data </span><span style="color:#D73A49">=</span><span style="color:#24292E"> password.encode() </span><span style="color:#D73A49">+</span><span style="color:#005CC5"> bytes</span><span style="color:#24292E">(</span><span style="color:#005CC5">12</span><span style="color:#D73A49"> -</span><span style="color:#005CC5"> len</span><span style="color:#24292E">(password.encode()) </span><span style="color:#D73A49">%</span><span style="color:#005CC5"> 12</span><span style="color:#24292E">)  </span><span style="color:#6A737D"># always at least one terminating zero byte</span></span>
<span class="line"><span style="color:#24292E">encoded </span><span style="color:#D73A49">=</span><span style="color:#005CC5"> bytes</span><span style="color:#24292E">(b </span><span style="color:#D73A49">^</span><span style="color:#005CC5"> KEY</span><span style="color:#24292E">[i </span><span style="color:#D73A49">%</span><span style="color:#005CC5"> len</span><span style="color:#24292E">(</span><span style="color:#005CC5">KEY</span><span style="color:#24292E">)] </span><span style="color:#D73A49">for</span><span style="color:#24292E"> i, b </span><span style="color:#D73A49">in</span><span style="color:#005CC5"> enumerate</span><span style="color:#24292E">(data))</span></span>
<span class="line"><span style="color:#24292E">fd </span><span style="color:#D73A49">=</span><span style="color:#24292E"> os.open(</span><span style="color:#032F62">"/etc/kcpassword"</span><span style="color:#24292E">, os.</span><span style="color:#005CC5">O_WRONLY</span><span style="color:#D73A49"> |</span><span style="color:#24292E"> os.</span><span style="color:#005CC5">O_CREAT</span><span style="color:#D73A49"> |</span><span style="color:#24292E"> os.</span><span style="color:#005CC5">O_TRUNC</span><span style="color:#24292E">, </span><span style="color:#D73A49">0o</span><span style="color:#005CC5">600</span><span style="color:#24292E">)</span></span>
<span class="line"><span style="color:#D73A49">with</span><span style="color:#24292E"> os.fdopen(fd, </span><span style="color:#032F62">"wb"</span><span style="color:#24292E">) </span><span style="color:#D73A49">as</span><span style="color:#24292E"> f:</span></span>
<span class="line"><span style="color:#24292E">    f.write(encoded)</span></span>
<span class="line"><span style="color:#24292E">os.chown(</span><span style="color:#032F62">"/etc/kcpassword"</span><span style="color:#24292E">, </span><span style="color:#005CC5">0</span><span style="color:#24292E">, </span><span style="color:#005CC5">0</span><span style="color:#24292E">)</span></span>
<span class="line"><span style="color:#24292E">os.chmod(</span><span style="color:#032F62">"/etc/kcpassword"</span><span style="color:#24292E">, </span><span style="color:#D73A49">0o</span><span style="color:#005CC5">600</span><span style="color:#24292E">)</span></span>
<span class="line"><span style="color:#24292E">subprocess.run([</span><span style="color:#032F62">"defaults"</span><span style="color:#24292E">, </span><span style="color:#032F62">"write"</span><span style="color:#24292E">, </span><span style="color:#032F62">"/Library/Preferences/com.apple.loginwindow"</span><span style="color:#24292E">,</span></span>
<span class="line"><span style="color:#032F62">                "autoLoginUser"</span><span style="color:#24292E">, user], </span><span style="color:#E36209">check</span><span style="color:#D73A49">=</span><span style="color:#005CC5">True</span><span style="color:#24292E">)</span></span>
<span class="line"></span></code></pre>
<p>After a reboot, <code>stat -f %Su /dev/console</code> prints the service user.</p>
<h2 id="full-disk-access-over-ssh-and-screen-sharing-are-turned-on-in-the-macs-system-settings-not-from-the-command-line">Full disk access over SSH and Screen Sharing are turned on in the Mac's System Settings, not from the command line</h2>
<p>Full disk access is a macOS privacy permission (TCC, Transparency, Consent and Control, whose database System Integrity Protection guards), which the command line cannot grant, even with sudo, and Screen Sharing turned on from the command line could not capture the screen. Both are set once in System Settings on the mini when it is set up; full disk access can also come from an MDM profile.</p>
<div class="blog-table" role="region" aria-label="Table 14" tabindex="0"><table>
<thead>
<tr>
<th>Setting, in System Settings</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td>General &gt; Sharing &gt; the (i) next to Remote Login &gt; Allow full disk access for remote users on. On macOS versions without that switch: Privacy &amp; Security &gt; Full Disk Access &gt; +, then Cmd-Shift-G and <code>/usr/libexec/sshd-keygen-wrapper</code></td>
<td>maintenance runs over SSH, and without it <code>du</code>, <code>ls</code> and the like get <code>Operation not permitted</code> in Desktop, Documents, Downloads, the Trash and other protected folders, even with sudo, so disk use cannot be fully accounted for</td>
</tr>
<tr>
<td>General &gt; Sharing &gt; Screen Sharing on; under its (i), Allow access for: Only these users, the account the worker runs as; &quot;VNC viewers may control screen with password&quot; off</td>
<td>System Settings, and anything else that needs the desktop, can be reached without going to the mini; clients authenticate with the macOS account</td>
</tr>
</tbody>
</table>
</div><p>On a mini reached through Tailscale SSH, the session runs under <code>tailscaled</code> rather than macOS's <code>sshd</code>, so the Remote Login switch, which grants <code>sshd</code>, does not cover it. Granting full disk access to <code>tailscaled</code> itself (for the Homebrew build, <code>/opt/homebrew/opt/tailscale/bin/tailscaled</code>) is the likely equivalent, but we have not yet confirmed that it opens protected folders over Tailscale SSH.</p>
<p>Screen Sharing turned on over SSH (<code>launchctl enable</code> and <code>bootstrap</code> of <code>com.apple.screensharing</code>) opened port 5900, but the client got a black screen, and after the display was woken <code>screensharingd</code> still logged that it could not capture the screen; we turn Screen Sharing on in System Settings instead. While Remote Management is on, the Screen Sharing switch is greyed out as controlled by the Remote Management service. Remote Management grants much broader remote administration, so we turn it off and plain Screen Sharing on.</p>
<p>If a client shows a black screen, the mini's display may be asleep; <code>caffeinate -u -t 5</code> over SSH wakes it. From Linux, Remmina with its VNC plugin (<code>sudo apt install remmina remmina-plugin-vnc</code>) connects to the mini's Tailscale IP and logs in with the macOS account.</p>
<h2 id="an-unattended-mini-needs-host-and-deploy-settings-that-survive-reboots-crashes-and-failed-deploys">An unattended mini needs host and deploy settings that survive reboots, crashes and failed deploys</h2>
<div class="blog-table" role="region" aria-label="Table 15" tabindex="0"><table>
<thead>
<tr>
<th>Setting</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td>The worker is a LaunchDaemon (system domain) with <code>UserName</code> set, not a LaunchAgent</td>
<td>it starts at boot and needs no login; MLX, Metal, MPS and the Neural Engine all work in that context</td>
</tr>
<tr>
<td>FileVault off, auto-login for the service user, <code>pmset</code> <code>autorestart 1</code> and <code>sleep 0</code></td>
<td>an unattended mini must boot after a power loss into a logged-in session without anyone typing a password, and never sleep; auto-login requires FileVault off. Anyone with physical access gets an unencrypted disk and a logged-in session</td>
</tr>
<tr>
<td>Spotlight indexing off (<code>sudo mdutil -a -i off</code>)</td>
<td>every job writes and deletes audio files under the work directory, and indexing them would spend CPU, memory and disk writes the jobs need</td>
</tr>
<tr>
<td>Wi-Fi off on a mini with Ethernet (<code>networksetup -setairportpower en1 off</code>)</td>
<td>Ethernet and Wi-Fi on the same subnet gave the mini two default routes; the setting persists across reboots</td>
</tr>
<tr>
<td><code>KeepAlive</code> and <code>ProcessType Interactive</code> in the plist</td>
<td>launchd restarts a crashed worker and applies no background resource limits; the scheduler re-queues a dead worker's jobs after a 60 s grace period</td>
</tr>
<tr>
<td><code>PATH</code> in the plist includes <code>/opt/homebrew/bin</code></td>
<td>launchd services and non-login SSH sessions do not get Homebrew's path, and ffmpeg lives there</td>
</tr>
<tr>
<td><code>ExitTimeOut</code> of 3600 s, and SIGTERM drains</td>
<td>a deploy's <code>launchctl bootout</code> lets running jobs finish instead of killing them mid-upload</td>
</tr>
<tr>
<td>Each remote deploy command prints its own exit status on a marker line</td>
<td>Tailscale SSH reported exit status 0 for failed remote commands</td>
</tr>
<tr>
<td>The scheduler stores an expected release per worker</td>
<td>a rolling deploy updates one mini at a time, and the minis not yet updated stay valid and keep taking jobs</td>
</tr>
<tr>
<td>A smoke restore runs after the release symlink switches and before the service restarts; a failure switches back</td>
<td>a release that cannot load the model or restore a file never takes jobs</td>
</tr>
<tr>
<td>Code ships as a <code>git archive</code>, weights from a bundle on the deploy host</td>
<td>the minis hold no GitHub or Hugging Face credentials</td>
</tr>
<tr>
<td>The model loads before the worker offers any slot (11–16 s; on a new machine the Neural Engine's first compile of all 4.5 graphs takes about 5.5 minutes)</td>
<td>a job never waits on a cold model</td>
</tr>
</tbody>
</table>
</div><h2 id="sources">Sources</h2>
<ul>
<li><a href="https://diffio.ai/media/blog/mac-mini-inference-serving/mac-mini-serving-measurements.json">Diffio Mac mini serving measurements</a>: the dev job timings, burst runs with per-job marks, the GPU batch sweep and its fit, the model-concurrency runs, per-process memory, file-descriptor counts before and after the leak fixes, inference times per file and the host figures behind the tables. The stem upload benchmark, the ScoreQ timing and the macOS 27 <code>audiomxd</code> sample come from separate runs on the same minis and are not in this file; thresholds and deploy settings come from our worker's code and configuration.</li>
<li>Diffio production telemetry, October 13–14, 2026 (not published): per-stage fleet job timings from our edge scheduler's job events (the 60 s pro split, the wired and Wi-Fi mini comparison), separated-track timing from the workers' logs, the traced API job and transcription times from backend events, and the R2 stall counts from Cloud Run request logs, in the routing, callback, API-path and R2 sections; the uplink figures are 25 MB uploads to Cloudflare's speed test endpoint from each mini. Queue, routing, upload-limit, memory and timeout settings come from our worker, edge and backend code.</li>
<li>Diffio fleet operations, October 21–22, 2026 (not published): one mini's used space before and after a macOS update changed the build, which folders stayed unreadable over SSH with and without full disk access, which process serves an SSH session on each mini, and Screen Sharing enabled over SSH, in the disk and the full disk access and Screen Sharing sections.</li>
<li><a href="https://docs.aws.amazon.com/botocore/latest/reference/config.html">botocore Config reference</a>: <code>connect_timeout</code> and <code>read_timeout</code> default to 60 seconds, and <code>tcp_keepalive</code> only sets the socket option, in the R2 section.</li>
<li><a href="https://ffmpeg.org/ffmpeg-protocols.html">FFmpeg protocols documentation</a>: the <code>rw_timeout</code> protocol option, in the R2 section.</li>
<li><a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/setrlimit.2.html">setrlimit(2), Apple man page</a>: the rejection of an unlimited <code>RLIMIT_NOFILE</code> soft limit and the <code>min(OPEN_MAX, rlim_max)</code> recommendation, in the open-file section.</li>
<li><a href="https://keith.github.io/xcode-man-pages/launchd.plist.5.html">launchd.plist(5)</a>: <code>UserName</code>, <code>KeepAlive</code>, <code>ProcessType</code>, <code>ExitTimeOut</code>, <code>EnvironmentVariables</code>, and <code>SoftResourceLimits</code> setting <code>kern.maxfiles</code> for a daemon, in the open-file section and the settings table.</li>
<li><a href="https://developer.apple.com/documentation/coreml/downloading-and-compiling-a-model-on-the-user-s-device">Core ML: Downloading and compiling a model on the user's device</a>: compiled models land in a temporary location and should be moved to a permanent one, in the startup-sweep section.</li>
<li><a href="https://support.apple.com/en-us/102316">How to log in automatically to a Mac user account, Apple Support</a>: FileVault disables automatic login, in the settings table.</li>
</ul>
<h2 id="related-diffio-posts">Related Diffio posts</h2>
<ul>
<li><a href="https://diffio.ai/blog/diffio-apple-silicon/">Partitioning MLPs Along the Intermediate Dimension Let the Apple Neural Engine Run Two of Three Guidance Branches</a>: how one restore divides its guidance branches between the GPU, the CPU and the Neural Engine.</li>
<li><a href="https://diffio.ai/blog/classifier-free-guidance/">Classifier-Free Guidance: The Guidance Scale Is an Exponent on an Implicit Classifier</a>: why each sampler step evaluates several guidance branches.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Partitioning MLPs Along the Intermediate Dimension Let the Apple Neural Engine Run Two of Three Guidance Branches</title>
      <link>https://diffio.ai/blog/diffio-apple-silicon/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/diffio-apple-silicon/</guid>
      <pubDate>Wed, 23 Sep 2026 15:03:54 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>ML</category>
      <category>Inference</category>
      <category>Apple Silicon</category>
      <description>How Diffio 4.0 reached 7.8x (pro) and 10.7x (flash) realtime on an M4 Pro Mac mini by splitting guidance branches across the GPU, the CPU&apos;s SME units and the Neural Engine, and what memory, precision and scheduling taught us.</description>
      <media:content url="https://diffio.ai/media/blog/diffio-apple-silicon/feature.webp" medium="image"/>
      <content:encoded><![CDATA[<p>We ported Diffio 4.0, our diffusion-transformer <a href="https://diffio.ai/how-it-works/">speech restoration model</a>, to a Mac mini with an M4 Pro (12-core CPU, 16-core GPU, 24 GB of unified memory). It restores audio at 7.8x realtime with the pro sampler and 10.7x with flash, against about 68x for pro on an RTX PRO 6000 in our CUDA deployment, and matches the CUDA run's quality within seed noise on word error rate, speaker similarity and UTMOS. Each guided sampler step runs on the GPU, the CPU's SME matrix units and the Neural Engine at once. The Neural Engine could take its share only after every MLP in its copy of the model was partitioned along the intermediate dimension into independent blocks, which more than doubled its MLP throughput.</p>
<div class="blog-table" role="region" aria-label="Table 1" tabindex="0"><table>
<thead>
<tr>
<th>Per 52.96 s benchmark clip, M4 Pro</th>
<th>pro</th>
<th>flash</th>
</tr>
</thead>
<tbody>
<tr>
<td>Restore time</td>
<td>6.75 s</td>
<td>4.93 s</td>
</tr>
<tr>
<td>Realtime factor</td>
<td>7.8x</td>
<td>10.7x</td>
</tr>
</tbody>
</table>
</div><h2 id="a-guided-step-is-three-independent-forward-passes">A guided step is three independent forward passes</h2>
<p>The model is a diffusion transformer with a Qwen2 trunk. It restores audio in overlapping 30 s windows, each conditioned on semantic features from the Granite speech encoder, on observation latents (the degraded input encoded by the Ming AudioVAE codec) and on the transcript text. The samplers are stochastic rectified-flow samplers (<a href="https://diffio.ai/blog/flow-matching/">flow matching</a> covers the velocity field they integrate) with <a href="https://diffio.ai/blog/classifier-free-guidance/">classifier-free guidance</a>. A guided step evaluates three branches: u (no conditions), so (semantic and observation, no text) and sot (all conditions). Late steps are unguided and evaluate sot alone.</p>
<div class="blog-table" role="region" aria-label="Table 2" tabindex="0"><table>
<thead>
<tr>
<th>Model and sampler</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Layers, hidden size, MLP intermediate size</td>
<td>28, 1536, 8960 (SwiGLU)</td>
</tr>
<tr>
<td>Positions per window (375 latent frames plus prompt)</td>
<td>381</td>
</tr>
<tr>
<td>Cost of one forward pass</td>
<td>about 1.05 TFLOP</td>
</tr>
<tr>
<td>Window length and advance</td>
<td>30 s, 23 s (a benchmark clip is two windows)</td>
</tr>
<tr>
<td>Steps per window, pro / flash</td>
<td>12 (7 guided, 5 unguided) / 8 (4 and 4)</td>
</tr>
</tbody>
</table>
</div><p>The branches do not interact until the guidance arithmetic combines their velocities, so a step can be divided by branch across engines and lasts as long as the slowest engine's share.</p>
<div class="blog-table" role="region" aria-label="Table 3" tabindex="0"><table>
<thead>
<tr>
<th>Engine</th>
<th>How we program it</th>
<th>Matrix throughput</th>
</tr>
</thead>
<tbody>
<tr>
<td>GPU</td>
<td>MLX, bf16 GEMMs</td>
<td>5.75 TFLOPS measured, 6.46 peak</td>
</tr>
<tr>
<td>CPU SME units (Scalable Matrix Extension)</td>
<td>MLX CPU stream (Accelerate), fp32</td>
<td>3.3–3.7 TFLOPS measured alone</td>
</tr>
<tr>
<td>Neural Engine</td>
<td>Core ML, fp16</td>
<td>4.2 TFLOPS on our MLP as first converted; about 19 peak through private APIs</td>
</tr>
</tbody>
</table>
</div><pre class="blog-code"><code>guided step    (~235 ms)   GPU + CPU       sot    each MLP split between GPU (bf16) and CPU (fp32)
                           Neural Engine   so, u  one after the other
unguided step  (~215 ms)   GPU + CPU       sot
                           Neural Engine   codec work for neighbouring windows
per window                 GPU (PyTorch)   Granite, the condition encoders and the text encoder
</code></pre>
<h2 id="mlxs-gemms-reach-89-of-the-gpus-peak-so-kernels-were-not-the-bottleneck">MLX's GEMMs reach 89% of the GPU's peak, so kernels were not the bottleneck</h2>
<p>Moving the transformer from PyTorch's MPS backend to MLX was the first large gain, because MLX fuses the elementwise work around each GEMM into fewer kernels; it also cut the relative L2 error of the output velocity against an fp32 forward pass from 6e-2 to 5e-3. Compiling each layer's attention block with <code>mx.compile</code> helped a little more. The GEMMs themselves had nothing left to give: Metal 4's tensor-op multiply (<code>matmul2d</code>) ran within 1% of MLX at our shapes, and PyTorch's <code>torch.compile</code> made the MPS forward pass 70% slower.</p>
<h2 id="splitting-each-mlp-between-the-gpu-and-the-cpus-sme-units-makes-all-cpu-work-compete-with-the-model">Splitting each MLP between the GPU and the CPU's SME units makes all CPU work compete with the model</h2>
<p>MLX evaluates a GPU stream and a CPU stream concurrently. We partition each MLP along its intermediate dimension: the GPU computes 70% of the intermediate columns in bf16, the CPU's SME units compute the other 30% in fp32 (bf16 elementwise math measured four times slower than fp32 on the CPU), and the two partial down-projections are summed. In a microbenchmark this cut an unguided forward pass from 236 ms to 208 ms. The best share, found by sweep, gives the GPU more than the standalone rates would, because neither engine keeps its standalone rate while both run: they share one power budget, and the GPU's clock drops from 1578 MHz to 1.43–1.54 GHz while the CPU computes.</p>
<p>Because the CPU is part of every layer, any other CPU work lengthens the steps it overlaps. The codec decoder's final ISTFT stage cost 110 ms of numpy per window, and moving it to the GPU (11 ms) sped up the steps it had overlapped. A helper process computing the next window's Granite features on the CPU would have cost more than it saved, and on the efficiency cores at background priority the same work was far too slow.</p>
<h2 id="core-ml-runs-the-transformer-on-the-neural-engine-only-as-fixed-shape-fp16-chunks-with-overflow-guards">Core ML runs the transformer on the Neural Engine only as fixed-shape fp16 chunks with overflow guards</h2>
<p>Core ML, the only public route to the Neural Engine, compiles fixed-shape fp16 graphs for it, and when its compiler rejects a graph it runs it on the CPU without an error. The full 28-layer model and our first codec conversion (rank-5 tensors in its attention) both fell back; Core ML's compute plan (<code>MLComputePlan</code>, which reports each operation's device) and power readings (the Neural Engine at 0 W) catch it. The text-free branches run as chunks of consecutive layers. The small pieces around the chunks (input projection, timestep embedding, per-layer adaptive-norm vectors, output projection) run in numpy in a separate process, because Core ML's <code>predict</code> holds Python's GIL.</p>
<p>fp16 tops out at 65504, and this model exceeds it in attention-sink scores and in the squares inside RMSNorm. The conversion normalizes each RMSNorm row by its maximum absolute value before squaring; for attention it scales the queries by an extra 1/1024, subtracts each row's maximum, and scales back before the softmax. Both rewrites are exact in fp32. With them the Neural Engine's branches are as accurate as the GPU's: on inputs captured from real guided steps, relative L2 error against fp32 is 4.7–6.4e-3 on the Neural Engine and 5.2–7.5e-3 for GPU bf16.</p>
<p>The GPU's error comes mostly from its bf16 GEMM inputs: the same forward pass with fp16 GEMMs and an fp32 residual stream is 1.9–2.6e-3 from fp32. The Neural Engine gains fp16's three extra mantissa bits in its GEMMs and gives most of them back by keeping the residual stream in fp16. Quantizing its weights to int8 would have doubled the error for a 6% speedup.</p>
<h2 id="converting-granite-exposed-two-fp16-precision-losses-activations-fused-after-a-gemm-and-the-overflow-guard">Converting Granite exposed two fp16 precision losses: activations fused after a GEMM, and the overflow guard</h2>
<p>A SiLU that directly follows a GEMM on the Neural Engine loses precision that the same SiLU alone does not: in a probe, relative error was 2.8e-4 standalone and 2.9e-3 after a linear layer. On Granite's own activations, writing the sigmoid as <code>1/(1+exp(-x))</code> brought a feed-forward block from 3.9e-3 to 2.4e-3, where tanh after a linear layer had reached 1.8e-2. The overflow guard costs precision where it is not needed: without it, Granite's attention error fell from 3.9e-2 to 1.6e-3. The likely cause is that the extra 1/1024 puts small query values near fp16's smallest normal number, where fewer mantissa bits remain. The transformer's text-free branches need the guard and did not change when their SiLU and softmax were rewritten.</p>
<h2 id="a-branch-without-text-attends-to-one-null-token-so-its-cross-attention-is-a-constant">A branch without text attends to one null token, so its cross-attention is a constant</h2>
<p>A branch without transcript text still has a text cross-attention input: a learned null embedding, with every other position masked. A softmax over a single unmasked key is 1 for every query, so the cross-attention output is the null token's value projection passed through the output projection, a constant vector per layer. The text-free branches skip the query projection, the attention and the per-position output projection, and text keys and values are computed only for branches that have text. In a single forward pass the conditioned branch stayed bit-identical and the unconditional branch moved by at most one bf16 unit in the last place. The shortcut also makes so and u the same graph: their prompts have the same shape and differ only in token ids, whose embeddings are computed outside the Core ML chunks.</p>
<h2 id="partitioning-the-mlp-along-its-intermediate-dimension-let-the-neural-engine-run-two-branches">Partitioning the MLP along its intermediate dimension let the Neural Engine run two branches</h2>
<p>Before partitioning, one text-free branch took 244 ms on the Neural Engine, and batching two branches into one call cost 2.17x the time of one, so the GPU kept two branches per guided step. A microbenchmark of one MLP at our shape showed where the time went: partitioned into independent blocks of intermediate columns, with the blocks' down-projections summed, the same MLP ran more than twice as fast at the same error against fp32.</p>
<div class="blog-table" role="region" aria-label="Table 4" tabindex="0"><table>
<thead>
<tr>
<th>MLP at 381 positions, fp16 weights</th>
<th>Weights per block</th>
<th>Throughput</th>
</tr>
</thead>
<tbody>
<tr>
<td>SwiGLU, 1 block</td>
<td>82.6 MB</td>
<td>4.2 TFLOPS</td>
</tr>
<tr>
<td>SwiGLU, 4 blocks</td>
<td>20.6 MB</td>
<td>9.7 TFLOPS</td>
</tr>
<tr>
<td>SwiGLU, 8 blocks</td>
<td>10.3 MB</td>
<td>10.1 TFLOPS</td>
</tr>
<tr>
<td>ReLU MLP as 1x1 convolutions, 1 block</td>
<td>55.1 MB</td>
<td>3.3 TFLOPS</td>
</tr>
<tr>
<td>ReLU MLP, 2 blocks</td>
<td>27.5 MB</td>
<td>6.8 TFLOPS</td>
</tr>
<tr>
<td>ReLU MLP, 4 blocks</td>
<td>13.8 MB</td>
<td>11.5 TFLOPS</td>
</tr>
</tbody>
</table>
</div><p>Throughput rises until a block's weights plus its activations fit in about 32 MB, then flattens. That is the size of the Neural Engine's on-chip SRAM, where reverse-engineering measurements found throughput falling once a matrix multiply's operands no longer fit, although at our shapes the penalty was larger than the 30% they report. The ReLU MLP at 2 blocks, 27.5 MB of weights but about 33 MB with activations, sits between the extremes. We did not observe the compiler's schedule, so this reading fits the data without proving the cause. Apple's guidance for transformers on the Neural Engine recommends chunking large intermediate tensors, which the partition also does, for cache residency and to spread work across the engine's cores.</p>
<div class="blog-table" role="region" aria-label="Table 5" tabindex="0"><table>
<thead>
<tr>
<th>Neural Engine text-free branch</th>
<th>Time</th>
</tr>
</thead>
<tbody>
<tr>
<td>Before partitioning (4-layer chunks)</td>
<td>244 ms</td>
</tr>
<tr>
<td>MLP in 8 blocks</td>
<td>142 ms</td>
</tr>
<tr>
<td>7-layer chunks instead of 4</td>
<td>130 ms</td>
</tr>
<tr>
<td>MLP as 1x1 convolutions on (B, C, 1, S)</td>
<td>119 ms</td>
</tr>
<tr>
<td>4 blocks instead of 8</td>
<td>117 ms</td>
</tr>
</tbody>
</table>
</div><p>Partitioning alone was worth 1.7x; chunks longer than 7 layers and more than 8 blocks did not help. At 117 ms per branch, the Neural Engine runs so and then u while the GPU and CPU run sot, and guided steps fell from about 355 ms to about 235 ms. Clips gained less than that step saving predicts, flash least: the codec work that had filled the Neural Engine's idle time on guided steps lost it, and flash has the fewest unguided steps to absorb it.</p>
<h2 id="duplicate-weight-copies-not-the-model-made-unified-memory-swap">Duplicate weight copies, not the model, made unified memory swap</h2>
<p>Unified memory removes copies between engines, not copies within a program. With the Neural Engine running two branches, mid-run free memory fell to 13% and the system swapped 5–6 GB. The main process held 14 GB, mostly GPU memory (MLX's weights and buffers plus PyTorch's allocator), including original MLP weights that MLX kept after building the split GPU and CPU copies. Each set of Core ML chunks also holds its own weights, and u had its own set. Running u on so's chunks and dropping the original MLP weights kept swap under 0.7 GB and removed most of the run-to-run timing variance. Capping MLX's cache of freed buffers, and <code>mx.set_wired_limit</code>, which keeps MLX's buffers resident so the memory compressor never touches the weights, bought a few percent more.</p>
<p>Memory also blocked the remaining engine move. Unguided steps would take 156 ms on the Neural Engine instead of 215 ms on the GPU, but they condition on text and need a third set of chunks, which brought back 2–4 GB of swap. Core ML multifunction packages, which let two fixed-shape functions share one deduplicated weight file, reduced that to transient swap, and the configuration then only tied the default on this 24 GB machine.</p>
<h2 id="the-codec-runs-in-neural-engine-idle-time-that-the-main-process-declares">The codec runs in Neural Engine idle time that the main process declares</h2>
<p>The codec encodes each window's input into the observation condition and decodes each finished window into audio. Both run in the Neural Engine process as Core ML chunks of 30–60 ms, and a chunk starts only while the main process has declared the Neural Engine idle: for 0.18 s on each unguided step, when the GPU works alone, and for 0.5 s after each window. Each window is decoded while the next one samples, and the next window's observation is encoded ahead of time. A file's first window has no earlier window to overlap, so its encode starts at once and runs while the GPU computes the Granite features.</p>
<p>Declared idle time still costs something. Running Granite on the Neural Engine as well removed its work from the GPU but made clips slower overall. We did not isolate the cause; the most likely one is the CPU work every Neural Engine call brings (dispatch and conversion of large fp32 arrays), which slows the split MLP like any other CPU work. Splitting one branch's layers between the GPU and the Neural Engine failed the same way: guided steps got faster, but the per-step hand-off and the lost idle time for the codec left clip time unchanged.</p>
<h2 id="measured-effect-of-each-change-on-clip-time">Measured effect of each change on clip time</h2>
<p>Each entry comes from an interleaved A/B at the time, repeated until the direction held (run-to-run spread was 0.1–0.3 s per clip); the entries are not strictly additive.</p>
<div class="blog-table" role="region" aria-label="Table 6" tabindex="0"><table>
<thead>
<tr>
<th>Change</th>
<th>Clip time</th>
</tr>
</thead>
<tbody>
<tr>
<td>Transformer from PyTorch MPS to MLX</td>
<td>22.7 s to 18.2 s</td>
</tr>
<tr>
<td>30% of each MLP on the CPU's SME units</td>
<td>−5.7%</td>
</tr>
<tr>
<td>Codec decode on the Neural Engine in its idle time</td>
<td>−9.3%</td>
</tr>
<tr>
<td>Production samplers' branch sets (three branches guided, one unguided)</td>
<td>−5%</td>
</tr>
<tr>
<td><code>mx.compile</code> per attention block</td>
<td>−3%</td>
</tr>
<tr>
<td>Next window's codec encode ahead of time</td>
<td>about −4%</td>
</tr>
<tr>
<td>Null-text cross-attention shortcut (u still on the GPU)</td>
<td>−4%</td>
</tr>
<tr>
<td>First window's encode started at once</td>
<td>−2.4%</td>
</tr>
<tr>
<td>Codec ISTFT stage moved from CPU to GPU</td>
<td>−2.5%</td>
</tr>
<tr>
<td>Partitioned MLPs; Neural Engine runs so and u</td>
<td>pro −13%, flash −4%</td>
</tr>
<tr>
<td>so and u share one set of Core ML chunks; no duplicate MLP weights</td>
<td>−10%</td>
</tr>
<tr>
<td>MLX buffer-cache cap; wired memory</td>
<td>−2%; −4% (pro)</td>
</tr>
<tr>
<td>Rewrite without a GPU worker thread, text keys and values computed once per window</td>
<td>6.98 s to 6.75 s</td>
</tr>
<tr>
<td>Granite on the Neural Engine (rejected)</td>
<td>+0.5 s</td>
</tr>
<tr>
<td>Next window's Granite on the CPU (rejected)</td>
<td>about +9% at real duty</td>
</tr>
<tr>
<td>Unguided steps on the Neural Engine, 24 GB (rejected)</td>
<td>tie at best</td>
</tr>
</tbody>
</table>
</div><h2 id="quality-matches-the-cuda-run-except-a-scoreq-offset-that-predates-the-neural-engine">Quality matches the CUDA run except a ScoreQ offset that predates the Neural Engine</h2>
<p>The benchmark is the one the CUDA production run uses. ScoreQ-NR is a no-reference speech-quality predictor on a 1–5 scale; WER is the word error rate of an ASR transcript of the restored speech against the reference transcript; speaker similarity is the cosine between speaker embeddings of the restored and clean speech; UTMOS is a predicted mean opinion score. The Mac draws its sampler noise from CPU generators, so it does not reproduce CUDA's noise, and per-clip differences include seed variation of about ±0.01 for ScoreQ and speaker similarity and ±0.02 for WER. Intervals are bootstrap intervals over per-clip differences.</p>
<div class="blog-table" role="region" aria-label="Table 7" tabindex="0"><table>
<thead>
<tr>
<th>Mac minus CUDA (95% CI)</th>
<th>pro</th>
<th>flash</th>
</tr>
</thead>
<tbody>
<tr>
<td>ScoreQ-NR</td>
<td>−0.021 [−0.039, −0.007]</td>
<td>−0.013 [−0.024, −0.003]</td>
</tr>
<tr>
<td>WER</td>
<td>+0.002 [−0.003, +0.007]</td>
<td>−0.002 [−0.006, +0.002]</td>
</tr>
<tr>
<td>Speaker similarity</td>
<td>+0.010 [−0.001, +0.020]</td>
<td>+0.010 [−0.001, +0.020]</td>
</tr>
<tr>
<td>UTMOS</td>
<td>−0.003 [−0.018, +0.012]</td>
<td>+0.006 [−0.012, +0.024]</td>
</tr>
</tbody>
</table>
</div><p>Only the ScoreQ interval excludes zero, at one to two times the seed-to-seed noise. The first full run, before any branch ran on the Neural Engine, had the same offset (−0.025 pro, −0.018 flash), and it did not grow afterwards.</p>
<p>Every change was smoke-tested on two clips before its timing counted and re-scored on all 78 clips when it moved outputs beyond rounding. The final rewrite produces bit-identical audio to the benchmarked pipeline. From a cold start, a 3-minute file restores in 32 s, including compilation of the first window's graphs and excluding an 8 s model load.</p>
<h2 id="mac-mini-setup-changes">Mac mini setup changes</h2>
<div class="blog-table" role="region" aria-label="Table 8" tabindex="0"><table>
<thead>
<tr>
<th>Change</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td>Disable Spotlight indexing (<code>sudo mdutil -a -i off</code>)</td>
<td>the pipeline writes thousands of WAV files; indexing them churns CPU and memory during runs</td>
</tr>
<tr>
<td>Watch <code>VTDecoderXPCService</code> (<code>top -o mem</code>) and kill it when large</td>
<td>it grew to 31 GB after a day of uptime and pushed 8.8 GB into the compressor; it relaunches on demand</td>
</tr>
<tr>
<td>Check <code>sysctl vm.swapusage</code> during a run</td>
<td>swapping slows every step with no other symptom</td>
</tr>
<tr>
<td>One restore process per machine</td>
<td>two processes made each clip 10–15x slower</td>
</tr>
<tr>
<td>Size the MLX wired limit to the machine (8 GB here)</td>
<td>wired memory is unavailable to everything else</td>
</tr>
<tr>
<td>A separate Python environment with PyTorch 2.7 for Core ML conversion</td>
<td>coremltools 9.0 is tested only up to PyTorch 2.7; the runtime uses 2.10</td>
</tr>
<tr>
<td>Allow about 20 s the first time each Core ML model loads, and keep the compiled <code>.mlmodelc</code></td>
<td>the Neural Engine compiles every model on first use</td>
</tr>
<tr>
<td>Guard every entry point with <code>if __name__ == &quot;__main__&quot;:</code></td>
<td>the Neural Engine process is started with <code>spawn</code>, which re-imports the main module</td>
</tr>
<tr>
<td><code>ffmpeg</code> on <code>PATH</code>; <code>PYTORCH_ENABLE_MPS_FALLBACK=1</code> for scoring</td>
<td>input decoding; the benchmark's band metrics use <code>torch.logspace</code>, which MPS lacks</td>
</tr>
</tbody>
</table>
</div><p>The software was macOS 27, MLX 0.32, coremltools 9.0 and PyTorch 2.10. Core ML models were converted as fp16 ML programs for macOS 15 with CPU-and-Neural-Engine compute units and take fp32 NumPy inputs. Arrays cross the pipe to the Neural Engine process at under 1 MB per guided step, plus about 5 MB of conditions per window. These balances were measured on the M4 Pro; the M5 generation adds matrix units to the GPU, which would shift them.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://machinelearning.apple.com/research/neural-engine-transformers">Apple Machine Learning Research, Deploying Transformers on the Apple Neural Engine (2022)</a>: the (B, C, 1, S) layout with 1x1 convolutions and chunking of intermediate tensors for cache residency and multicore use, in the partitioning section.</li>
<li><a href="https://maderix.substack.com/p/inside-the-m4-apple-neural-engine-615">Inside the M4 Apple Neural Engine, Part 2: ANE Benchmarks</a>: the ~32 MB SRAM cliff and the ~30% penalty for matrix multiplies whose operands exceed it, in the partitioning section.</li>
<li><a href="https://arxiv.org/abs/2603.06728">Orion: Characterizing and Programming Apple's Neural Engine for LLM Training and Inference (arXiv:2603.06728)</a>: the 32 MB SRAM, the ~19 TFLOPS fp16 peak through private APIs and fp16's range, in the engine table and the Core ML section.</li>
<li><a href="https://arxiv.org/abs/2409.18779">Hello SME! Generating Fast Matrix Multiplication Kernels Using the Scalable Matrix Extension (arXiv:2409.18779)</a>: the M4 CPU's SME units, in the CPU section.</li>
<li><a href="https://ml-explore.github.io/mlx/build/html/python/_autosummary/mlx.core.set_wired_limit.html">MLX documentation: <code>mlx.core.set_wired_limit</code></a> and <a href="https://ml-explore.github.io/mlx/build/html/usage/compile.html">compilation</a>: wired memory and <code>mx.compile</code>, in the memory and MLX sections.</li>
<li><a href="https://apple.github.io/coremltools/docs-guides/source/multifunction-models.html">Core ML Tools: multifunction models</a>: weight sharing between functions, in the memory section.</li>
<li><a href="https://machinelearning.apple.com/research/exploring-llms-mlx-m5">Apple Machine Learning Research, Exploring LLMs with MLX and the Neural Accelerators in the M5 GPU</a>: the M5 GPU's matrix units, in the setup section.</li>
</ul>
<h2 id="related-diffio-posts">Related Diffio Posts</h2>
<ul>
<li><a href="https://diffio.ai/blog/classifier-free-guidance/">Classifier-Free Guidance: The Guidance Scale Is an Exponent on an Implicit Classifier</a>: why a guided step needs forward passes with and without conditions.</li>
<li><a href="https://diffio.ai/blog/flow-matching/">Flow Matching: Regress the Field the Sampler Integrates</a>: the velocity field the samplers integrate.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Smaller Model State Did Not Establish Faster Cold Requests for Diffio on Modal</title>
      <link>https://diffio.ai/blog/efficient-model-deployment-modal/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/efficient-model-deployment-modal/</guid>
      <pubDate>Wed, 09 Sep 2026 00:42:46 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>ML</category>
      <category>Inference</category>
      <category>Modal</category>
      <description>What Diffio’s Modal experiments revealed about GPU snapshots, initialization, quantization, CUDA graphs, transcription dependencies, and cold-request latency.</description>
      <content:encoded><![CDATA[<p>Diffio’s deployment experiments ran from July through September 2026 on our <a href="https://diffio.ai/">audio-restoration service</a>. Diffio 2.0 uses ahead-of-time compiled model packages; Diffio 3.5 also has an autoregressive language-model path and a diffusion component. The general <a href="https://diffio.ai/blog/vibevoice-next-token-diffusion/">next-token diffusion architecture</a> explains why the autoregressive loop and diffusion head offer different optimization boundaries.</p>
<div class="blog-table" role="region" aria-label="Table 1" tabindex="0"><table>
<thead>
<tr>
<th>September research environment</th>
<th>Configuration</th>
</tr>
</thead>
<tbody>
<tr>
<td>GPU</td>
<td>RTX PRO 6000 Blackwell</td>
</tr>
<tr>
<td>Runtime</td>
<td>PyTorch 2.9, CUDA 12.8</td>
</tr>
</tbody>
</table>
</div><h2 id="a-cold-request-includes-work-that-a-gpu-profiler-cannot-see">A cold request includes work that a GPU profiler cannot see</h2>
<p>Our restoration workflow includes source preparation, transcription, dispatch, platform waiting, worker startup, model execution, publication, and a completion join that waits for all required outputs. Some stages overlap, so adding their elapsed times can double-count the same wall-clock interval.</p>
<div class="blog-table" role="region" aria-label="Table 2" tabindex="0"><table>
<thead>
<tr>
<th>Measurement</th>
<th>What it establishes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Snapshot creation</td>
<td>The cost of building a reusable initialized worker state.</td>
</tr>
<tr>
<td>Restored cold request</td>
<td>The first request handled by a newly restored worker.</td>
</tr>
<tr>
<td>Same-process warm request</td>
<td>Reuse of a worker that has already handled a request.</td>
</tr>
<tr>
<td>First playable audio</td>
<td>In the final study, the client downloaded and decoded the first AAC segment; this did not measure audible browser playback.</td>
</tr>
<tr>
<td>Complete request</td>
<td>The instrumented client observed the complete workflow, including required transcript and artifact publication.</td>
</tr>
<tr>
<td>GPU kernel interval union</td>
<td>The elapsed union of GPU kernel intervals, counting overlapping kernels once. It excludes CPU and remote waiting.</td>
</tr>
</tbody>
</table>
</div><p>A restore flag alone cannot classify a request as cold: the second request in that process still inherits a worker that originally restored from a snapshot. We required distinct worker identities, evidence that the previous worker was gone, and platform restore events. Snapshot-creation attempts remained in the attempt journal but outside the restored cohort.</p>
<p>The boundary labeled “physical start” also needed verification. The final harness preferred Modal’s restore-start system log; it was not invariably an operating-system process-creation timestamp. Its interval to the start of application finalization is therefore a restore-to-finalization-start proxy, not an isolated measurement of checkpoint copying. Submission to that event can include placement, image preparation, retries, and other platform work.</p>
<h2 id="gpu-snapshots-avoided-package-loading-and-scoreq-initialization">GPU snapshots avoided package loading and ScoreQ initialization</h2>
<p>GPU snapshots become valuable when restoring initialized state costs less than repeating the computation that constructed it. In the July probe, application finalization after a CPU snapshot took about 22.5 seconds, compared with about 0.4 seconds after a GPU-plus-CPU snapshot. That small historical comparison measured finalization, not total request latency, and used separate groups of six CPU-snapshot and eight GPU-snapshot observations.</p>
<p>Our early 2.0 worker still loaded its compiled packages on the first restored request, despite preparing their files during image construction. That first control trace spent 6.97 seconds loading the packages.</p>
<p>ScoreQ, the learned audio-quality scorer used by the service, had an even more expensive first-use path. Its first production-shape ONNX inference took roughly 35–36 seconds, overlapping word-to-audio alignment enough to make that stage look slow. Warming alignment alone did not remove the delay. Exercising ScoreQ’s actual input shape before capture moved its convolution-search and initialization work out of the first restored request.</p>
<p>After restoration, the worker created fresh credentials, network clients, runtime threads, and random identity. Retaining reusable computation while refreshing externally meaningful state avoided reusing captured connections or a creator’s random stream.</p>
<p><a href="https://modal.com/docs/guide/memory-snapshots">Modal’s current snapshot documentation</a> places pre-capture work in <code>@modal.enter(snap=True)</code> and describes GPU snapshots as an alpha feature. Snapshots use the same distributed storage system as Images and Volumes, so storage-bandwidth-limited weight loading may gain little.</p>
<p>Snapshot creation still has a cost. Our candidate spent longer preparing its initial reusable state, and creation can recur when code, configuration, or platform conditions change.</p>
<h2 id="the-earlier-bundle-lowered-median-completion-but-delayed-the-3-5-preview">The earlier bundle lowered median completion but delayed the 3.5 preview</h2>
<p>The September combined optimization changed several components together: compiled-package preload, ScoreQ warmup, redundant model copies, allocator state, CPU memory reservation, and the 3.5 diffusion execution path. Its 60-second-input comparison used ten matched restored pairs per model. This bundled design does not isolate any individual change’s contribution.</p>
<div class="blog-table" role="region" aria-label="Table 3" tabindex="0"><table>
<thead>
<tr>
<th>Model</th>
<th style="text-align:right">Median complete request, control → bundle</th>
<th style="text-align:right">Mean complete request, control → bundle</th>
<th style="text-align:right">Faster candidate pairs</th>
</tr>
</thead>
<tbody>
<tr>
<td>Diffio 2.0</td>
<td style="text-align:right">61.934 → 33.093 s</td>
<td style="text-align:right">64.100 → 33.124 s</td>
<td style="text-align:right">10/10</td>
</tr>
<tr>
<td>Diffio 3.5</td>
<td style="text-align:right">85.350 → 45.604 s</td>
<td style="text-align:right">85.232 → 85.243 s</td>
<td style="text-align:right">7/10</td>
</tr>
</tbody>
</table>
</div><p>The 3.5 mean stayed flat despite its lower median because capacity-related outliers remained. Median time to an advertised HLS (HTTP Live Streaming) playlist or segment increased from 35.892 to 46.096 seconds; this measured advertisement, not successful download and decoding. The preview subset contained eight observations per arm. The preview and completion medians use different subsets, so their relative ordering is not a per-request ordering.</p>
<h2 id="repository-pruning-did-not-measure-snapshot-shrinkage">Repository pruning did not measure snapshot shrinkage</h2>
<p>The model repository was our versioned release source, not a download repeated on every restored request. We published immutable Hugging Face revisions, verified their file hashes, and synchronized them into append-only Modal Volume directories. Image construction prepared compatible executables and caches. Restored trials ran with Hugging Face downloads disabled.</p>
<p>Removing unused legacy GPU-architecture packages saved about 4.04 GB in the 2.0 repository, but those files were never active state on the tested GPU. Pruning an unused semantic tokenizer and connector from 3.5 removed about 694 MB of parameters while leaving retained tensors unchanged. These studies measured repository, compiled-constant, or device-memory reductions; they did not measure serialized snapshot size.</p>
<p>Immutable revisions also protect restores and rollbacks. Hugging Face’s <a href="https://huggingface.co/docs/huggingface_hub/main/guides/download">download API</a> supports explicit commit revisions. Modal documents that changing a Volume does not invalidate its snapshots, and deleting files needed by a restore can break it. We therefore preserved old revision directories while live or rollback deployments could still depend on them.</p>
<p>The release identity combined source, model revision, image, and configuration. Development deployment and fixture checks preceded any promotion decision.</p>
<h2 id="compiled-packages-failed-after-the-server-passed-health-checks">Compiled packages failed after the server passed health checks</h2>
<p>Ahead-of-time compilation, which produces executables before serving, adds compatibility requirements beyond tensor shapes. Our packages targeted SM120, NVIDIA’s architecture identifier for the tested Blackwell GPU. A fallback GPU needed its own compatible package; an existing package for another NVIDIA architecture was not evidence of compatibility.</p>
<p>Three packaging failures only became visible when we exercised the real model:</p>
<ul>
<li><strong>System-library compatibility.</strong> The compiled shared libraries required <code>GLIBC_2.38</code>; the older image supplied glibc 2.36.</li>
<li><strong>Executable-stack flags.</strong> A newer image’s hardened loader rejected flags on compiled wrapper libraries. We cleared them in image-local copies, rebuilt and checked the archives, and left pinned source artifacts intact.</li>
<li><strong>Execution-provider selection.</strong> Dependencies could install both CPU and GPU ONNX Runtime distributions. The final build installed the pinned GPU distribution last and required <code>CUDAExecutionProvider</code>, the backend that runs supported ONNX operations on CUDA.</li>
</ul>
<p>The canary therefore loaded every required package and ran real audio. A successful import, HTTP health check, or tensor allocation could miss all three failures.</p>
<h2 id="cuda-graphs-reduced-diffusion-runtime-broader-compilation-added-first-use-work">CUDA graphs reduced diffusion runtime; broader compilation added first-use work</h2>
<p>A CUDA graph records a GPU execution sequence for replay with stable memory addresses, reducing repeated launch overhead. We captured the deterministic 3.5 diffusion trajectory, generated its initial random noise outside capture, and kept graph inputs private. Moving decoder cache indices to the CPU also avoided repeated GPU-to-CPU lookups.</p>
<p>The integrated service exposed a failure that isolated graph code missed: alignment performed CUDA work on another thread during global capture. Thread-local capture restricted unsafe-operation error checks to the capturing thread, resolving this observed capture failure while preserving a single denoising scheduler and private buffers. It was validated as a complete configuration; changing the capture mode alone does not make shared buffers safe. <a href="https://docs.pytorch.org/docs/2.9/generated/torch.cuda.graph.html">PyTorch’s capture-mode documentation</a> describes the different cross-thread error checks.</p>
<p>The selected graph matched baseline decoded PCM, the waveform’s numerical samples, across all 232 evaluation clips with frozen transcripts. Comparing WAV file bytes would have produced false differences because metadata timestamps changed. For a minute of input, median Diffio 3.5 local-runtime request time fell from 16.445 to 12.916 seconds. This included local transcription, alignment, and generation, and excluded the deployed storage, scoring, and gateway path.</p>
<p>Broader compilation did not produce the same result. Compiling the whole language-model path introduced roughly 117 seconds of first-use work and 62 seconds on a new shape. The tested whole-Whisper compilation also added substantial compilation overhead on first use and on new input shapes. Compilation cost alone would not reject a configuration whose compilation could finish before capture; these tested configurations did not establish a reduction in complete-request latency.</p>
<h2 id="quantized-storage-successful-reload-and-acceptable-audio-are-separate-gates">Quantized storage, successful reload, and acceptable audio are separate gates</h2>
<p>Post-training quantization replaces trained weights with lower-precision representations, optionally using calibration recordings to choose the mapping, without fine-tuning the model.</p>
<p>Our INT4 export initially failed to preserve packed storage. Tensor metadata could describe a low-bit object while copying into an ordinary floating-point destination reconstructed a larger representation. We inspected the physical packed backing storage and compiled constants, then reloaded each candidate in a fresh process. Earlier serialized candidates that reconstructed the expected tensor class had still failed output parity.</p>
<p>Even correct packing did not establish an audio-quality pass. We compared frozen inputs and transcript conditioning, first against the current deployed model and then against the original BF16 model to measure cumulative change. The full evaluation contained 232 clips; the final acoustic-INT4 candidate also passed a fresh 32-recording confirmation set.</p>
<p>Quality decisions used UTMOS and WVMOS, learned predictors of perceived speech quality, averaged into a dataset-balanced blend, together with ScoreQ as a separate learned score. The provisional limits allowed at most a 0.02 drop in each aggregate and a 0.05 drop on any dataset. An improved aggregate could not override a failed dataset threshold. Those automated gates did not establish perceptual identity; no human listening verdict was recorded. Recognition-based checks likewise could flag numerical-word disagreements but could not resolve them without checking what was actually spoken.</p>
<p>MLP refers to feed-forward multilayer perceptron sublayers. HQQ, GPTQ, and AWQ-lite name the tested quantization recipes; NVFP4 is a four-bit floating-point format.</p>
<div class="blog-table" role="region" aria-label="Table 4" tabindex="0"><table>
<thead>
<tr>
<th>Tested family</th>
<th>Result that determined the decision</th>
</tr>
</thead>
<tbody>
<tr>
<td>HQQ INT4 language projections, including smaller-group and MLP-only variants</td>
<td>Candidates that passed the initial small evaluation set failed broader quality evaluation.</td>
</tr>
<tr>
<td>Calibrated NVFP4 with GPTQ and INT4 with AWQ-lite</td>
<td>Successful packing and calibration still failed the tested audio-quality gates.</td>
</tr>
<tr>
<td>Selective INT8 language MLPs</td>
<td>Lost 0.283 blend points on SOVA despite a favorable aggregate.</td>
</tr>
<tr>
<td>Other tested lower-precision acoustic or codec variants</td>
<td>Tested variants failed quality or execution checks.</td>
</tr>
<tr>
<td>Acoustic MLPs changed from INT8 to INT4, retaining INT8 attention</td>
<td>Advanced to restored cold requests.</td>
</tr>
</tbody>
</table>
</div><p>Positive deltas below favor the candidate.</p>
<div class="blog-table" role="region" aria-label="Table 5" tabindex="0"><table>
<thead>
<tr>
<th>Full evaluation comparison</th>
<th style="text-align:right">Blend delta</th>
<th style="text-align:right">ScoreQ delta</th>
</tr>
</thead>
<tbody>
<tr>
<td>Acoustic INT4 vs current deployed model</td>
<td style="text-align:right">+0.01678</td>
<td style="text-align:right">+0.01284</td>
</tr>
<tr>
<td>Acoustic INT4 vs original BF16 model</td>
<td style="text-align:right">+0.00514</td>
<td style="text-align:right">+0.00254</td>
</tr>
<tr>
<td>Worst dataset change vs original BF16</td>
<td style="text-align:right">−0.03587</td>
<td style="text-align:right">−0.02554</td>
</tr>
</tbody>
</table>
</div><p>These are component- and recording-specific results, not a ranking of quantization methods.</p>
<h2 id="the-final-smaller-state-candidates-did-not-meet-the-cold-latency-gate">The final smaller-state candidates did not meet the cold-latency gate</h2>
<p>The final study compared candidates against controls with the earlier deployment improvements already in place. Acceptance required the specified median completion improvement, a paired interval supporting improvement, passing quality, and no clear first-playable regression.</p>
<div class="blog-table" role="region" aria-label="Table 6" tabindex="0"><table>
<thead>
<tr>
<th>Final cold-study protocol</th>
<th>Requirement</th>
</tr>
</thead>
<tbody>
<tr>
<td>Input duration</td>
<td>60 seconds</td>
</tr>
<tr>
<td>Practical median completion target</td>
<td>At least 1 second and 5% faster</td>
</tr>
<tr>
<td>Planned main comparison</td>
<td>10 matched pairs per candidate</td>
</tr>
</tbody>
</table>
</div><p>Pairs used the same frozen input and workflow settings, interleaving control and candidate and reversing their order on alternating pairs. The allocator candidate asked ONNX Runtime to release unused ScoreQ arena memory while retaining its session and caches. The acoustic candidate quantized the original BF16 weights directly, avoiding requantization of the lossy INT8 representation.</p>
<div class="blog-table" role="region" aria-label="Table 7" tabindex="0"><table>
<thead>
<tr>
<th>Candidate</th>
<th style="text-align:right">Measured reduction</th>
<th style="text-align:right">Matched cold pairs</th>
<th style="text-align:right">Median paired completion change, with 95% bootstrap interval</th>
</tr>
</thead>
<tbody>
<tr>
<td>ScoreQ allocator shrink, 3.5</td>
<td style="text-align:right">512 MiB device memory in the local probe</td>
<td style="text-align:right">10</td>
<td style="text-align:right">−6.20 s [−63.00, +77.84]</td>
</tr>
<tr>
<td>Acoustic INT4 MLPs, 2.0</td>
<td style="text-align:right">130,170,880 compiled constant bytes</td>
<td style="text-align:right">7</td>
<td style="text-align:right">+5.74 s [+4.47, +66.32]</td>
</tr>
</tbody>
</table>
</div><p>Changes are candidate minus matched control; negative favors the candidate. The intervals describe the paired median and come from resampling whole matched pairs. The acoustic candidate’s interval describes a sample stopped under a futility rule introduced during collection; it does not have guaranteed fixed-sample coverage. Creator counters repeated after a restore are frozen metadata rather than new memory observations.</p>
<p>The allocator result was inconclusive. Its mean completion change was slower by 11.66 seconds, and its median restore-to-finalization-start proxy changed by only −0.096 seconds. The favorable completion median did not establish that less retained memory restored faster.</p>
<p>Acoustic-INT4 completion was slower in six of seven pairs, and median first-playable time also worsened. We stopped further dispatches when the planned ten-pair median could no longer improve, even with arbitrarily good remaining pairs. The best possible median was still +4.54 seconds. One request already in flight finished and remained unmatched, outside the paired statistics.</p>
<p>Neither candidate qualified for the planned additional cold tests at other input durations or a cold-latency release.</p>
<p>The acoustic candidate’s local warm slowdown was much smaller than its complete request difference. The alternating-process RunPod diagnostic measured model execution, excluding transcription, ScoreQ, Modal restoration, transport, and output-file writing.</p>
<div class="blog-table" role="region" aria-label="Table 8" tabindex="0"><table>
<thead>
<tr>
<th>Input duration</th>
<th style="text-align:right">Current model</th>
<th style="text-align:right">Acoustic INT4</th>
<th style="text-align:right">Added model time</th>
</tr>
</thead>
<tbody>
<tr>
<td>3 s</td>
<td style="text-align:right">2.126 s</td>
<td style="text-align:right">2.334 s</td>
<td style="text-align:right">0.208 s</td>
</tr>
<tr>
<td>30 s</td>
<td style="text-align:right">2.128 s</td>
<td style="text-align:right">2.305 s</td>
<td style="text-align:right">0.177 s</td>
</tr>
<tr>
<td>60 s</td>
<td style="text-align:right">2.131 s</td>
<td style="text-align:right">2.318 s</td>
<td style="text-align:right">0.187 s</td>
</tr>
<tr>
<td>300 s</td>
<td style="text-align:right">6.469 s</td>
<td style="text-align:right">7.034 s</td>
<td style="text-align:right">0.565 s</td>
</tr>
</tbody>
</table>
</div><p>The diagnostic made 48 calls. Excluding the first call per duration in each process left 32 calls, with four observations per table cell.</p>
<p>The compiled model processes fixed 120-second windows, padding shorter inputs. The three shorter fixtures therefore each execute one window; the longest executes three, explaining the nearly flat short-input timings.</p>
<p>We retained the candidate’s artifacts so its lower storage requirement and higher execution time could be reproduced. Attributing the entire cold-request regression to low-bit kernels would conflate the local compute penalty with platform and workflow variability.</p>
<h2 id="waiting-before-restoration-dominated-the-largest-observed-delay">Waiting before restoration dominated the largest observed delay</h2>
<p>One allocator-control request took about 845 seconds to complete, with roughly 776 seconds between GPU submission and the explicit restore-start log. Transcription had already completed while Modal showed a pending call with no worker task binding. After the restore event, the observed interval to finalization start was about six seconds.</p>
<p>The timing places most of that delay before the recorded restore event without identifying which platform operation consumed it. The tested state reductions did not establish an improvement in that waiting interval. Both arms experienced long waits, and we retained them in the paired sample.</p>
<p>An earlier region-pinned arm had worse scheduling observations and was stopped. A cheaper GPU fallback offered no measured advantage and introduced a different architecture to support. Reducing the CPU memory reservation was eventually viable for the changed bundle, but an earlier comparison had favored the larger reservation’s scheduling. Initialization peaks, steady memory, placement, and cost were separate constraints.</p>
<h2 id="independent-transcription-let-diffio-2-0-release-the-gpu-before-the-job-finished">Independent transcription let Diffio 2.0 release the GPU before the job finished</h2>
<p>Moving speech recognition off the GPU removed a separate ASR and alignment package, about 1.89 GB per model. ASR means automatic speech recognition; the acoustic Whisper encoders used by restoration still remained necessary. Single-run GPU profiles showed less kernel execution after offload, but paired warm service measurements showed slower completion because the worker waited for remote transcripts.</p>
<div class="blog-table" role="region" aria-label="Table 9" tabindex="0"><table>
<thead>
<tr>
<th>Approximately 32-second input</th>
<th style="text-align:right">Old warm completion mean</th>
<th style="text-align:right">External-ASR warm completion mean</th>
</tr>
</thead>
<tbody>
<tr>
<td>Diffio 2.0</td>
<td style="text-align:right">13.628 s</td>
<td style="text-align:right">29.076 s</td>
</tr>
<tr>
<td>Diffio 3.5</td>
<td style="text-align:right">22.145 s</td>
<td style="text-align:right">32.777 s</td>
</tr>
</tbody>
</table>
</div><p>Each mean uses two subsequent calls in the same process; the kernel profiles were separate single observations, not a performance distribution. Moving work to another service changed which resource performed it, without automatically removing it from the completion dependency graph.</p>
<p>Diffio 2.0 allowed restoration and transcription to execute independently because its restored audio does not require the external transcript. We dispatched audio restoration independently, let it publish and release its GPU invocation, and completed the job on the CPU after both audio and transcript were valid. Tests that deliberately held back transcript-provider responses verified that ordering for short and long inputs, rather than measuring a new latency distribution. Diffio 3.5 still depended on canonical transcript conditioning, so it could not inherit the same independence. An earlier evaluation using external transcripts also failed a dataset quality threshold.</p>
<p>The CPU completion coordinator checked that audio and transcript belonged to the same operation before marking the job successful. It prevented uncertain dispatch from duplicating GPU work, respected cancellation and later transcript edits, and charged usage once. Audio availability and full-job completion became separate states.</p>
<p>CPU preparation could remain slow even on a reused instance. One roughly ten-second extraction/upload interval occurred after that instance had handled 157 earlier requests. It did not support attributing the delay to an empty container’s initialization. Phase telemetry was needed to distinguish media work, storage round trips, and time between stages.</p>
<h2 id="retention-and-testing-budgets-depend-on-reuse-and-information-gained">Retention and testing budgets depend on reuse and information gained</h2>
<p>Keeping a container warm allows reuse if another request arrives before shutdown. In a small production sample of nine events, the shortest observed gap exceeded 22 minutes. That incomplete sample gave no evidence that retaining workers for a few additional minutes would often avoid a restore.</p>
<p>Modal’s <a href="https://modal.com/docs/guide/scale">autoscaler controls</a> expose minimum warm containers, an active buffer, and an idle scale-down window.</p>
<div class="blog-table" role="region" aria-label="Table 10" tabindex="0"><table>
<thead>
<tr>
<th>Retention estimate, September 8, 2026 listed rates</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Requested resources</td>
<td>RTX PRO 6000, 8 physical CPU cores, 32 GiB memory</td>
</tr>
<tr>
<td>Combined requested-resource rate</td>
<td>$0.001018/second</td>
</tr>
</tbody>
</table>
</div><p>Keeping that allocation for five extra minutes would cost about $0.31; actual metered usage and billing policy determine the invoice.</p>
<p>We used the already allocated RunPod GPU for calibration, packing, fresh reload, audio quality, profiling, and duration checks. Modal calls were reserved for the behavior that local processes could not reproduce: actual snapshot creation/restoration, platform waiting, and the complete deployed workflow. Interleaved controls, frozen revisions, explicit exclusions, and preserved failures made those paid calls interpretable.</p>
<p>The final three-app Modal billing checkpoint for the cold-quantization study was $4.43. It could lag usage and excluded RunPod, the separate CPU workflow, and external ASR.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://diffio.ai/media/blog/efficient-model-deployment-modal/diffio-modal-measurements-reviewed.json">Diffio deployment measurements</a>: original study methods and quality gates, anonymized final paired timings, initialization and packaging evidence, the earlier bundled comparison, warm-duration and transcription results, and the cold-study billing checkpoint. The experiments are dated; they are not a benchmark of every Modal configuration.</li>
<li><a href="https://modal.com/docs/guide/memory-snapshots">Modal Memory Snapshots</a>: lifecycle hooks, CPU/GPU snapshot behavior, storage-bandwidth limitations, captured randomness, and Volume dependencies in the snapshot and artifact sections.</li>
<li><a href="https://docs.pytorch.org/docs/2.9/generated/torch.cuda.graph.html">PyTorch 2.9 CUDA graph API</a>: replay and capture-mode semantics in the CUDA graph section.</li>
<li><a href="https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html">ONNX Runtime CUDA execution provider</a>: CUDA provider selection and convolution-algorithm search underlying the ScoreQ and packaging sections.</li>
<li><a href="https://huggingface.co/docs/huggingface_hub/main/guides/download">Hugging Face Hub downloads</a>: immutable revision selection in the artifact-release section.</li>
<li><a href="https://modal.com/docs/guide/scale">Modal scaling</a> and <a href="https://modal.com/pricing">pricing</a>: retention controls and the dated resource-cost calculation.</li>
</ul>
<h2 id="related-diffio-posts">Related Diffio Posts</h2>
<ul>
<li><a href="https://diffio.ai/blog/vibevoice-next-token-diffusion/">VibeVoice: Frame Rate Is the Context Budget for Long-Form TTS</a>: the general autoregressive-plus-diffusion architecture referenced above.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>TTS Models: Sampled Acoustic Representations Set the Generator Constraints</title>
      <link>https://diffio.ai/blog/tts-models-a-map-of-the-modern-speech-stack/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/tts-models-a-map-of-the-modern-speech-stack/</guid>
      <pubDate>Sat, 13 Jun 2026 15:43:43 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>TTS</category>
      <category>ML</category>
      <description>A map of modern text-to-speech systems by the representation their generators sample: codec tokens, mel frames, semantic-acoustic cascades, continuous latents, and hybrid frames.</description>
      <media:content url="https://diffio.ai/media/blog/tts-models-a-map-of-the-modern-speech-stack/voxtral-figure-2.webp" medium="image"/>
      <content:encoded><![CDATA[<p>Modern text-to-speech systems are easiest to place by naming the representation the primary acoustic generator samples before waveform rendering. A paper may call the model a codec language model, a diffusion TTS model, a speech LLM, or a flow-matching system, but the more durable question is what representation the system is trying to sample and on what schedule.</p>
<p>The prediction target is the first generator constraint. It strongly shapes the loss family, the sampler, the latency path, the representation boundary, and the kinds of errors that can compound at inference time. A generator that emits token IDs learns a categorical distribution. A generator that samples mel frames works over a continuous acoustic field, even if the network head predicts velocity or noise rather than the frame itself. A generator that emits semantic tokens still needs another model to render timbre, timing, prosody, and residual acoustic detail. A generator that emits continuous latents needs a sampler and a latent space that remain stable under prediction.</p>
<p>Backbone choice matters, but many recent TTS systems share familiar Transformer, DiT, Llama, Qwen, Ministral, or flow-matching machinery. The clearer first map is by sampled representation, with generation schedule tracked beside it.</p>
<h2 id="the-sampled-representation-constrains-the-generator">The sampled representation constrains the generator</h2>
<p>For multi-stage systems, track both the long-range sampled representation and the local acoustic-rendering representation. The sampled representation is neither the final waveform nor every internal tensor predicted by a neural network head. It is the representation that carries the acoustic modeling burden before a vocoder or codec decoder turns it into waveform.</p>
<p>Generation schedule is the second classification axis. Autoregressive, masked, non-autoregressive, delay-pattern, within-frame residual-depth, and ODE samplers create different critical paths over the same representation. Frame rate is not the same thing as total work or total bitrate.</p>
<div class="blog-table" role="region" aria-label="Table 1" tabindex="0"><table>
<thead>
<tr>
<th>Sampled representation before waveform rendering</th>
<th>Generator constraint</th>
<th>Typical systems</th>
<th>Primary design constraint</th>
</tr>
</thead>
<tbody>
<tr>
<td>Discrete codec token IDs</td>
<td>Sample codebook indices with a softmax, often across residual vector quantization depth</td>
<td>VALL-E, SoundStorm-style acoustic token stages, MOSS-TTS</td>
<td>Token rate, codebook depth, codec quality, generation schedule, and exposure to token errors</td>
</tr>
<tr>
<td>Continuous acoustic frames</td>
<td>Sample mel-spectrogram frames, often by predicting a vector field over those frames</td>
<td>Voicebox, Matcha-TTS, F5-TTS</td>
<td>Duration, alignment, ODE or diffusion step count, and vocoder quality</td>
</tr>
<tr>
<td>Semantic tokens followed by acoustic rendering</td>
<td>First sample content-biased units, then synthesize acoustic detail from them</td>
<td>CosyVoice, MaskGCT</td>
<td>Whether semantic tokens preserve enough lexical content, speech timing, prosody, and speaker information</td>
</tr>
<tr>
<td>Continuous latent vectors</td>
<td>Sample vectors from a learned low-rate audio latent space</td>
<td>LatentLM, VibeVoice, SemaVoice</td>
<td>Latent geometry, frame rate, vector width, variance modeling, and sampler robustness</td>
</tr>
<tr>
<td>Mixed per-frame bundles</td>
<td>Sample a low-rate semantic stream plus local acoustic variables inside each frame</td>
<td>Voxtral TTS, recent codec-LLM hybrids</td>
<td>How much global structure stays autoregressive and how much acoustic detail is delegated to a local generator</td>
</tr>
</tbody>
</table>
</div><p>The table separates sampled representations and sampler choices rather than ranking systems. A &quot;language model&quot; over audio can mean token IDs, continuous vectors, semantic codes, or a mixed frame. A &quot;diffusion&quot; or &quot;flow&quot; model can be the main generator, the acoustic renderer, or a local head attached to a token model. Semantic tokens are content-biased but can retain speaker and prosodic information.</p>
<h2 id="discrete-codec-tokens-make-tts-a-categorical-sequence-modeling-problem">Discrete codec tokens make TTS a categorical sequence-modeling problem</h2>
<p>Discrete codec TTS makes speech look like language modeling. A neural codec converts waveform into a grid of codebook IDs. The generator predicts those IDs, and the decoder converts a legal code stack back into audio.</p>
<p>VALL-E is the canonical version of this setup. It uses an EnCodec representation with 75 frames per second and eight residual vector quantization codebooks. VALL-E predicts the first codebook autoregressively and fills the remaining codebooks with non-autoregressive models.</p>
<p>Discrete codec tokens let the generator use cross-entropy, prompting, and sampling temperature, but quality and controllability remain bounded by codec rate, residual depth, and decoder behavior. A correct-looking token sequence can still sound wrong if the codec does not preserve the cue the task needs.</p>
<p>MOSS-TTS is the low-frame-rate, high-RVQ-depth case. It lowers the sequential frame rate to 12.5 frames per second while increasing residual quantization depth to 32 layers. That does not make acoustic modeling cost disappear. It changes the critical path from many temporal decisions to fewer temporal decisions with deeper within-frame reconstruction.</p>
<p>The codec-token design choice is how to allocate modeling capacity across temporal steps, residual codebooks, and within-frame decoding. Once the time axis is compressed, autoregressive temporal prediction is only one part of the problem. The model must also predict the residual-depth position within each frame.</p>
<h2 id="mel-frame-systems-generate-continuous-acoustic-features">Mel-frame systems generate continuous acoustic features</h2>
<p>Mel-frame systems leave discrete neural codec tokens out of the acoustic generator's sampled representation. The target is a dense time-frequency representation, usually an 80- or 100-bin log-mel frame sequence, and a separate vocoder turns that representation into waveform. The vocoder is not neutral: mel targets discard phase and rely on the vocoder prior for final waveform detail.</p>
<p>Voicebox, Matcha-TTS, and F5-TTS generate log-mel frames with non-autoregressive flow-matching models before a vocoder. Voicebox uses 80-dimensional log-mel frames at 100 Hz, and F5-TTS uses 100-channel log-mel frames before Vocos. Their inference cost is controlled by the number of function evaluations, or NFEs, rather than by autoregressive token count.</p>
<p>The model samples continuous acoustic features before vocoding. This avoids dependence on a codec codebook layout, but duration, alignment, conditioning, vector-field sampling, and vocoder behavior remain part of the design.</p>
<p>For TTS, flow matching shifts the design problem from vocabulary construction to vector-field modeling.</p>
<h2 id="semantic-token-cascades-separate-content-modeling-from-acoustic-rendering">Semantic-token cascades separate content modeling from acoustic rendering</h2>
<p>Semantic-token systems factor speech generation into semantic-token prediction and acoustic rendering. First, sample a content-biased representation. Then, render acoustics from that representation plus speaker, prompt, style, or text conditioning. &quot;Semantic&quot; can mean ASR-supervised units, self-supervised clusters, phoneme-like codes, or a semantic branch inside a codec.</p>
<p>CosyVoice is a representative supervised version. It extracts semantic tokens from an ASR-oriented model, uses a language model to generate those tokens from text, then uses a flow-matching acoustic model and HiFi-GAN vocoder to synthesize speech. MaskGCT is a masked-generation version: it uses a semantic tokenizer for content and an acoustic codec for waveform detail, then generates both stages with iterative masked prediction rather than a single left-to-right token pass.</p>
<p>The appeal is modularity. Text correctness and acoustic rendering are no longer forced into one token stream. Semantic tokens reduce the sequence length between text conditioning and acoustic rendering by replacing waveform-length acoustic detail with lower-rate content-biased units. The acoustic model can then focus on speaker identity, pitch, rhythm, and fine acoustic detail.</p>
<p>WER can overstate a semantic tokenizer's suitability for generative TTS. Word error rate, or WER, measures lexical recoverability but not whether the tokens preserve timing and acoustic correlations needed by a generator. The WER Trap paper shows speech tokens can score well on ASR-like tests while still failing to preserve the acoustic variation a generator needs. It proposes flow-probe diagnostics that ask whether a flow model can reconstruct acoustic trajectories from the tokens, not just whether an external recognizer can read them.</p>
<p>The generator constraint for a semantic cascade is two-sided. The first stage must preserve enough content to guide the utterance, and the second stage must be able to recover the missing acoustics without inventing the wrong speaker, rhythm, or affect.</p>
<h2 id="continuous-latents-replace-codebook-indices-with-vector-prediction-targets">Continuous latents replace codebook indices with vector prediction targets</h2>
<p>Continuous-latent TTS removes the categorical codebook interface. Instead of sampling token IDs, the generator samples vectors from a learned audio latent space. These are not just mel frames with a different name: mel bins are hand-designed acoustic features, while continuous latents are learned compressed vectors intended to be reconstructable and predictable. That makes the sampled representation a regression, diffusion, or flow problem rather than a softmax problem.</p>
<p>LatentLM is the cleanest general statement of this idea. It replaces next-token classification with next-token diffusion over continuous latents. For speech synthesis, it evaluates low-rate speech latents down to 3.75 Hz, making the context length look more like text while keeping the prediction target continuous.</p>
<p>VibeVoice is the long-form TTS example: a 7.5 Hz continuous acoustic latent plus a next-token diffusion head shifts the budget from sequence length to latent geometry. Its coupled shared-latent tokenizer ablation reduces perceptual speech quality (PESQ) and objective speaker similarity (SIM-O), which is why the related VibeVoice and latent-encoder posts treat rate, width, variance, and semantic alignment as generator constraints.</p>
<p>SemaVoice makes the same point from another direction. It uses continuous speech tokens at 15 Hz, and its frame-rate study shows that simply increasing temporal resolution is not automatically better. The denser 60 Hz setup degrades sharply in that study, especially without alignment. Without the right alignment and capacity, a denser latent stream can make prediction harder rather than easier.</p>
<p>A representation is both a compression format and the target distribution the generator must learn.</p>
<h2 id="hybrid-frames-combine-autoregressive-semantic-tokens-with-local-acoustic-generation">Hybrid frames combine autoregressive semantic tokens with local acoustic generation</h2>
<p>Recent systems increasingly combine the earlier choices rather than choosing only one. Voxtral TTS is a useful marker. Its codec runs at 12.5 Hz and represents each frame with one 8192-entry semantic VQ token plus 36 finite-scalar-quantized acoustic coordinates. The decoder backbone generates the semantic stream autoregressively. A flow-matching acoustic head models those 36 acoustic coordinates in continuous space, runs eight NFEs by default, then discretizes the result back to the 21-level FSQ interface.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/tts-models-a-map-of-the-modern-speech-stack/voxtral-figure-2.webp" alt="Voxtral TTS combines autoregressive semantic generation with a flow-matching acoustic token head. The figure is Figure 2, page 3, from the Voxtral TTS technical report." width="1006" height="833" loading="lazy" decoding="async"></figure>

<p>Voxtral combines a codec representation with a flow-matching acoustic head. The semantic stream keeps a language-model-style global ordering. The acoustic head avoids making all fine acoustic detail a long sequence of categorical decisions. Streaming then becomes a coordination problem between the autoregressive semantic loop, the local acoustic generator, and the audio decoder.</p>
<p>MOSS and Voxtral address the same residual acoustic modeling problem with different allocations: MOSS models RVQ depth within each frame, while Voxtral models local acoustic coordinates with a per-frame flow model. VibeVoice offers another answer by keeping the low-rate acoustic representation continuous.</p>
<p>Reducing sequential frame rate relocates acoustic detail into residual depth, a semantic-acoustic cascade, a local flow head, a mel-frame sampler, or a continuous latent space.</p>
<h2 id="five-fields-locate-a-tts-system">Five fields locate a TTS system</h2>
<p>A new TTS system can be classified by five fields.</p>
<ol>
<li>Which representation does the primary acoustic generator sample before waveform rendering?</li>
<li>Which generation schedule determines the critical path for that representation?</li>
<li>At what frame rate does the system make sequential decisions?</li>
<li>Where is acoustic detail represented when the frame rate is low?</li>
<li>Which metric tests the representation itself rather than only the final audio?</li>
</ol>
<p>These fields distinguish systems that share a frame rate, training objective, or tokenizer metric. A 12.5 Hz codec-token model and a 12.5 Hz hybrid flow model share a temporal rate, but they do not share a sampler or loss. A flow-matching mel model and a flow-matching acoustic-token head share training math, but not the sampled representation. A semantic tokenizer with strong WER is not automatically a good TTS latent. A continuous latent encoder that reconstructs well is not automatically easy for a language model to predict.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://arxiv.org/abs/2301.02111">VALL-E</a>: discrete-codec TTS, 75 Hz EnCodec representation, eight residual codebooks, and autoregressive plus non-autoregressive codebook generation.</li>
<li><a href="https://arxiv.org/abs/2306.15687">Voicebox</a>: non-autoregressive flow matching over 80-dimensional log-mel frames at 100 Hz and the NFE tradeoff for sampling.</li>
<li><a href="https://arxiv.org/abs/2309.03199">Matcha-TTS</a>: optimal-transport conditional flow matching for non-autoregressive acoustic modeling.</li>
<li><a href="https://arxiv.org/abs/2410.06885">F5-TTS</a>: direct non-autoregressive flow matching over 100-channel log-mel frames with Vocos as vocoder.</li>
<li><a href="https://arxiv.org/abs/2407.05407">CosyVoice</a>: supervised semantic tokens, text-to-token language modeling, and flow-matching acoustic rendering.</li>
<li><a href="https://arxiv.org/abs/2409.00750">MaskGCT</a>: semantic and acoustic token stages with masked generative codec Transformers.</li>
<li><a href="https://arxiv.org/abs/2603.18090">MOSS-TTS</a>: low-rate 12.5 Hz codec tokens, 32 residual codebooks, and residual-depth modeling.</li>
<li><a href="https://arxiv.org/abs/2603.25551">Voxtral TTS</a>: hybrid semantic-token plus finite-scalar-quantized acoustic-code architecture, Figure 2, Voxtral Codec, and per-frame flow-matching acoustic generation.</li>
<li><a href="https://arxiv.org/abs/2508.19205">VibeVoice</a>: 7.5 Hz semantic and continuous acoustic tokenizers, next-token diffusion head, and tokenizer ablations.</li>
<li><a href="https://arxiv.org/abs/2605.29209">The WER Trap</a>: evidence that semantic-token WER can miss generative acoustic suitability.</li>
<li><a href="https://arxiv.org/abs/2412.08635">LatentLM</a>: next-token diffusion over continuous latents and low-rate speech-latent experiments.</li>
<li><a href="https://arxiv.org/abs/2605.16964">SemaVoice</a>: continuous autoregressive speech synthesis and frame-rate sensitivity for continuous speech tokens.</li>
</ul>
<h2 id="related-diffio-posts">Related Diffio posts</h2>
<ul>
<li><a href="https://diffio.ai/blog/flow-matching/">Flow Matching</a></li>
<li><a href="https://diffio.ai/blog/moss-tts-rvq-depth/">MOSS-TTS: RVQ Depth Is The Bottleneck</a></li>
<li><a href="https://diffio.ai/blog/vibevoice-next-token-diffusion/">VibeVoice: Next-Token Diffusion For Long-Form Speech</a></li>
<li><a href="https://diffio.ai/blog/training-latent-audio-encoders/">Training Latent Audio Encoders</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Latent Audio Encoders: Reconstruction Sets the Ceiling, Not the Ranking</title>
      <link>https://diffio.ai/blog/training-latent-audio-encoders/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/training-latent-audio-encoders/</guid>
      <pubDate>Sat, 13 Jun 2026 00:12:05 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>Audio</category>
      <category>ML</category>
      <description>Why an audio autoencoder&apos;s reconstruction quality sets the ceiling for the generator trained on its latents, but does not decide which latent space ranks best.</description>
      <media:content url="https://diffio.ai/media/blog/training-latent-audio-encoders/longcat-figure-3.webp" medium="image"/>
      <content:encoded><![CDATA[<p>A continuous-latent audio autoencoder compresses waveform into a sequence of real-valued vectors that a downstream generator, a flow-matching DiT or an autoregressive LM with a per-token diffusion head, learns to predict from text or context; the decoder then turns predicted latents back into audio. That deployment loop stresses properties reconstruction training never measures: the generator must predict latent vectors from conditioning alone, and an autoregressive generator additionally feeds its own sampled latents back as context, so its errors compound through whatever geometry the latent space has.</p>
<p>Reconstruction quality still matters, as a ceiling: nothing generated through the decoder can sound better than the encoder-decoder round trip, so across large fidelity gaps it ranks systems coarsely. It cannot order competitive candidates: four sources show this directly by holding the downstream model fixed and varying only the latent space, and the autoencoder that reconstructs best is routinely not the one whose latents generate best. The remaining design axes, frame rate and width, regularization, semantic alignment, and training schedule, each target properties that reconstruction training does not measure.</p>
<h2 id="four-fixed-generator-comparisons-decouple-reconstruction-from-generation">Four fixed-generator comparisons decouple reconstruction from generation</h2>
<p>Each source trains one downstream generator configuration per latent space, so the latent is the only deliberate variable.</p>
<div class="blog-table" role="region" aria-label="Table 1" tabindex="0"><table>
<thead>
<tr>
<th>Generator held fixed</th>
<th>What varies in the latent</th>
<th>Reconstruction</th>
<th>Generation</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td>~1.4B flow-matching DiT, fixed 50k-step budget</td>
<td>VAE vs soft-normalization bottleneck (fixed normalization statistics in place of a sampled posterior), both at 4096x temporal downsampling, 256-dim</td>
<td>identical (MELlog1p 0.108 for both)</td>
<td>FAD-CLAP 0.651 vs 1.061</td>
<td>SAME, Table 2, p. 8</td>
</tr>
<tr>
<td>same</td>
<td>conventional 1024x/64-dim VAE vs auxiliary-loss-shaped 4096x/256-dim</td>
<td>best in the table (0.098) vs 0.109</td>
<td>0.724 vs 0.576</td>
<td>SAME, Table 2, p. 8</td>
</tr>
<tr>
<td>1B TTS DiT retrained per VAE</td>
<td>latent dimension 64 to 256 at fixed 20 Hz</td>
<td>STOI, speaker similarity, and PESQ improve; UTMOS dips</td>
<td>TTS WER, UTMOS, DNSMOS all degrade</td>
<td>LongCat-AudioDiT, Sec. 5.3.2, Fig. 3</td>
</tr>
<tr>
<td>one text-to-sound diffusion model</td>
<td>KL target swept across a 7.65–74.10 kbps family</td>
<td>rises with bitrate along the rate-distortion curve (Fig. 1)</td>
<td>peaks at 11.56 kbps (text-audio similarity 70.67, KAD 1.70), degraded at 74.10 kbps (66.84, 2.16)</td>
<td>Target-KL, Table 2</td>
</tr>
<tr>
<td>F5-TTS Small</td>
<td>vanilla VAE vs the same VAE aligned to WavLM, a self-supervised speech representation model; both 40 Hz/64-dim</td>
<td>equivalent (PESQ 3.75 vs 3.74)</td>
<td>WER 2.65 vs 1.95, SIM 0.60 vs 0.64</td>
<td>Semantic-VAE, Tables 1-2</td>
</tr>
</tbody>
</table>
</div><p>The first two rows come from one SAME ablation. Metrics: MELlog1p is a multi-resolution log-mel reconstruction distance (lower is better); FAD-CLAP is Fréchet audio distance in the embedding space of CLAP, a contrastive audio-text model (lower is better); STOI is a 0–1 intelligibility measure and PESQ a reference-based speech quality score, both computed against the original audio; WER is word error rate of an ASR model run on the synthesized speech; UTMOS and DNSMOS are model-predicted mean-opinion quality scores; text-audio similarity is a CLAP-embedding match score and KAD a kernel-based distribution distance between real and generated audio embeddings; SIM is speaker similarity, the cosine between speaker-embedding vectors.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/training-latent-audio-encoders/longcat-figure-3.webp" alt="LongCat-AudioDiT Figure 3: four panels plotting VAE reconstruction metrics and TTS metrics against latent dimension." width="1166" height="724" loading="lazy" decoding="async"></figure>

<p>LongCat-AudioDiT, Figure 3, p. 11. Solid lines are the Wav-VAE's reconstruction metrics, dashed lines the paired TTS model's, against latent dimension; the intelligibility panel plots negated WER, so falling means worse. The decoupling is sharpest in the intelligibility and quality panels; TTS speaker similarity peaks at 128 before dropping at 256, and the naturalness metrics decline on both sides.</p>
<p>The rows differ in what they isolate. The SAME pair puts the disagreement in distributional structure that the bottleneck's fixed statistics do not capture; <a href="https://diffio.ai/blog/same-audio-encoder/">the SAME post</a> traces which auxiliary losses put that structure in. LongCat shows the failure is not a generator-capacity problem: aside from a marginal speaker-similarity gain, scaling the TTS backbone to 3.5B on the 128-dim VAE still loses to 3.5B on the 64-dim VAE, so under a fixed training recipe the extra width is a modeling burden that added parameters do not offset. Target-KL shows the disagreement inside a single architecture family swept along one axis, which rules out confounds between recipes. And Semantic-VAE shows the converse: a latent change that improves generation substantially while reconstruction does not move at all.</p>
<p>The direction of the trade is not universal: AudioLDM's mel-spectrogram VAE reconstructs comparably across its compression sweep while generation quality falls as compression rises, the opposite of LongCat's frame-rate result below, so the optimum is task- and architecture-dependent. The durable conclusion is the selection rule: only a paired downstream model ranks latents as generation targets. Every sweep above paid for a full generator per candidate; whether a small jointly trained probe would produce the same ranking at a fraction of the cost is untested.</p>
<h2 id="frame-rate-and-width-set-the-prediction-problem">Frame rate and width set the prediction problem</h2>
<p>Frame rate and channel width are fixed before any training loss applies, and they define the object the generator must predict: rate sets the sequence length and how much information each step carries, width sets the dimensionality of the continuous vector the diffusion head or distribution predictor must model per step. Sequence length is what an autoregressive generator pays for in context and compute, which is why long-form TTS systems push rates down to 7.5–15 Hz; <a href="https://diffio.ai/blog/vibevoice-next-token-diffusion/">the VibeVoice post</a> works out that budget arithmetic. The two axes convert: in LatentLM's compression ablation, dropping the frame rate from 37.5 to 15 Hz at fixed width costs reconstruction similarity (0.866 to 0.700), and doubling the width restores it (0.870), with downstream TTS similarity following the same pattern. Information removed from the time axis must be carried by added latent dimensions.</p>
<p>Two sweeps locate the best shape for autoregressive speech at the same information rate, 480 floats per second:</p>
<div class="blog-table" role="region" aria-label="Table 2" tabindex="0"><table>
<thead>
<tr>
<th>Sweep (both at 480 floats/s)</th>
<th style="text-align:right">3.75 Hz</th>
<th style="text-align:right">7.5 Hz</th>
<th style="text-align:right">15 Hz</th>
<th style="text-align:right">30 Hz</th>
<th style="text-align:right">60 Hz</th>
</tr>
</thead>
<tbody>
<tr>
<td>LatentLM: TTS speaker similarity (dims 128/64/32)</td>
<td style="text-align:right">0.598</td>
<td style="text-align:right">0.656</td>
<td style="text-align:right">0.697</td>
<td style="text-align:right"></td>
<td style="text-align:right"></td>
</tr>
<tr>
<td>SemaVoice: TTS word error rate % (dims 32/16/8)</td>
<td style="text-align:right"></td>
<td style="text-align:right"></td>
<td style="text-align:right">2.97</td>
<td style="text-align:right">3.24</td>
<td style="text-align:right">14.71</td>
</tr>
</tbody>
</table>
</div><p>LatentLM, Tables 4-5; SemaVoice, Table 4. LatentLM sweeps downward from 15 Hz and loses speaker similarity as the rate falls; SemaVoice sweeps upward and loses intelligibility as the rate rises, while its reconstruction stays roughly stable. Jointly the two bracket 15 Hz as the optimum at this budget, approached from opposite directions; the sweeps differ in data, benchmark, backbone, and head, so they do not trace a controlled curve. And because the budget is fixed, rate and width move together: the 60 Hz failure happens in 8-dim frames, so the high end cannot separate a rate ceiling from a width floor. LongCat's rate sweep points the same way from the non-autoregressive side: lowering the frame rate at fixed dimension 64 costs VAE reconstruction similarity and PESQ while improving the paired TTS model, except for TTS speaker similarity, which peaks at 11.72 Hz, the rate LongCat ships.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/training-latent-audio-encoders/longcat-figure-4.webp" alt="LongCat-AudioDiT Figure 4: four panels plotting VAE reconstruction metrics and TTS metrics against latent frame rate." width="1171" height="720" loading="lazy" decoding="async"></figure>

<p>LongCat-AudioDiT, Figure 4, p. 12. Solid lines are the Wav-VAE's reconstruction metrics, dashed lines the paired TTS model's, against latent frame rate; the quality panel shows the two moving in opposite directions, and the similarity panel the TTS peak at the middle rate.</p>
<p>Width gets two defensible answers. LongCat's response to its dimension sweep is to keep the latent narrow, 64-dim at 11.72 Hz. SAME keeps a wide 256-dim latent at a frame rate close to LongCat's and restores predictability with auxiliary semantic and generation losses. LongCat's sweep varies width alone and measures the cost of raw width; SAME demonstrates that shaped width is usable, though its own table complicates a pure width story: the unshaped wide VAE already beats the narrow configuration on generation, a comparison that confounds width with frame rate. No source varies width and shaping independently. Geometry beyond the uniform time grid is also in play: Music2Latent2 packs each audio chunk into a set of unordered summary embeddings so global features are not re-encoded at every frame, and TADA collapses 50 Hz frames into one vector per text token, trading the time grid for text alignment.</p>
<h2 id="regularization-decides-which-component-absorbs-prediction-error">Regularization decides which component absorbs prediction error</h2>
<p>A predicted latent is always slightly off the encoder's output manifold. Regularization choices decide which component absorbs that error, and the systems here give four answers that shipping recipes combine.</p>
<p><strong>The latent distribution absorbs it.</strong> A vanilla VAE lets per-channel posterior variances collapse toward zero, leaving sampled latents brittle as autoregressive context. A sigma-VAE takes the variance out of the encoder's hands, training the decoder and the generator under latent noise of a designed scale so that prediction errors smaller than that scale stay in-distribution; the <a href="https://diffio.ai/blog/exposure-bias/">exposure bias post</a> develops the mechanism and its evidence. LatentLM, VibeVoice, and SemaVoice all build their speech tokenizers on the recipe.</p>
<p><strong>The decoder absorbs it.</strong> Injecting Gaussian noise into latents during autoencoder training builds the same robustness into a different component, and it is nearly free. LatentFlowSR's ablation isolates the effect: under inference-time latent noise its plain autoencoder collapses (LSD 3.42, ViSQOL 2.36, where LSD is log-spectral distance, lower better, and ViSQOL a 1–5 perceptual quality estimate) while the noise-trained version barely moves (0.92, 4.22), and on clean audio the noise-trained autoencoder reconstructs slightly better, not worse. SAME trains with latent noise at 5e-2 and keeps a smaller 1e-3 noise on the latent at inference.</p>
<p><strong>The generator absorbs it.</strong> An under-regularized latent reconstructs well but is sharp and perturbation-sensitive, so the full difficulty lands on the predictor; an over-regularized latent is smooth but caps fidelity. Most recipes pick a point on that trade implicitly through an ad hoc KL weight (Stable Audio Open down-weights KL by 1e-4 and accepts whatever rate emerges); Target-KL replaces the weight with a specified information rate, a squared penalty pulling measured KL toward a chosen value, which is what made the sweep in the table above possible. Its TTS results carry one caveat: some high-bitrate VAEs scored deceptively low WER because the diffusion model copied information from unregularized prompt latents, with monotonous-sounding output. KALL-E occupies the deliberately smooth end, keeping a high KL weight (32) and accepting worse reconstruction for a latent its autoregressive distribution predictor can fit.</p>
<p><strong>A separate pathway absorbs it.</strong> VibeVoice routes semantics around the generated latent entirely: its acoustic tokenizer stays purely reconstructive, and at each step a frozen ASR-trained encoder re-encodes the audio just generated and adds the result to the autoregressive history.</p>
<p>SAME goes one step further: rather than only making errors survivable, it trains a small flow-matching model jointly with the encoder and backpropagates that model's loss, so the latent space itself is optimized to be easy for a generator to model. <a href="https://diffio.ai/blog/same-audio-encoder/">The SAME post</a> covers the mechanism; <a href="https://diffio.ai/blog/flow-matching/">flow matching</a> is the objective both that probe and most of the downstream generators here train with.</p>
<h2 id="semantic-alignment-strength-must-match-latent-capacity">Semantic alignment strength must match latent capacity</h2>
<p>Semantic objectives change what the latent encodes rather than how much, and whether they conflict with acoustic fidelity depends on the alignment's strength and the latent's capacity, not on semantic alignment per se.</p>
<p>A hard semantic objective on a small bottleneck degrades both sides. VibeVoice pushed a full transcript-decoding ASR objective through a 7.5 Hz, 64-dim latent and collapsed reconstruction and speaker similarity together (PESQ 1.92 against 2.98 for its reconstruction-only tokenizer); the mechanism, two objectives competing for fixed capacity, is analyzed in <a href="https://diffio.ai/blog/vibevoice-next-token-diffusion/">the VibeVoice post</a>. The WER Trap measures the same competition from the semantic side with a discrete 5 Hz ASR-supervised token: adding a reconstruction loss costs character error rate (11.98 to 14.32) and audio-QA accuracy. Both land on decoupling the objectives.</p>
<p>A gradient-bounded alignment loss improves both sides. SemaVoice aligns its 15 Hz latent to frozen WavLM features with a frame-wise cosine loss plus a pairwise self-similarity loss, and sets the alignment weight each step so its gradient norm is half the reconstruction loss's, so the alignment can never dominate. It improves WER and speaker similarity together, costs almost nothing on reconstruction, and its value grows with frame rate: at 60 Hz, where the aligned sweep has already degraded to WER 14.71, removing the alignment doubles the damage to 28.06. Loss form matters as much as weight: in Semantic-VAE, cosine alignment to a late WavLM layer improves TTS, while L1 and MSE alignment to the same teacher are worse than no alignment at all (WER 3.12 and 4.37 against the 2.65 baseline); magnitude-constraining losses over-restrict the latent where directional alignment only orients it. CALM extends to its full continuous latent the WavLM cosine distillation that Mimi, the discrete tokenizer it derives from, applies only to its first codebook.</p>
<p>A weak objective on a wide latent is nearly free. SAME applies linear probes regressing DSP features plus a contrastive text critic to its 256-dim latent and takes the best generation scores in its ablation at a small reconstruction cost. The availability of a teacher explains why the recipes differ across domains: speech has WavLM-class self-supervised content teachers, so SemaVoice, Semantic-VAE, and CALM align to them; music has no equivalent, so SAME builds its semantic targets from the signal (chromagrams, interaural level difference) and from paired text. No single source varies alignment strength at fixed rate and width, so where soft becomes hard, and how width moves that threshold, remains unmeasured.</p>
<h2 id="staged-training-routes-adversarial-gradients-away-from-the-encoder">Staged training routes adversarial gradients away from the encoder</h2>
<p>The sources here disagree about freezing schedules but agree about routing: losses that shape the latent for generation (semantic alignment, SAME's diffusion probe) are deliberately sent into the encoder, while adversarial and feature-matching gradients that fit fine-grained waveform detail are kept off it during late fine-tuning. RAVE shows why: it trains its VAE for representation first, then freezes the encoder and fine-tunes only the decoder adversarially, and its appendix shows that letting the stage-two feature-matching loss reach the encoder dramatically inflates the latent space's estimated dimensionality (by SVD, its 128 nominal dimensions hold about 16–24 informative ones when the encoder is protected). RAVE measures the harm as lost compactness, not generation quality, but spreading information across more dimensions is exactly the raw width whose cost the LongCat sweep measured. The pattern at scale: Stable Audio Open trains end to end with adversarial losses for 183 hours, then spends 273 hours training the decoder alone against a frozen encoder, and SAME freezes its encoder for two decoder-only stages.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://arxiv.org/abs/2605.18613">SAME: A Semantically-Aligned Music Autoencoder</a>: the fixed-budget bottleneck ablation rows in the decoupling table (Table 2, Sec. 5.1); noise injection scales (Sec. 2.3); the linear-probe and contrastive losses and the joint diffusion probe (Sec. 3.3); frozen-encoder decoder stages (Sec. 4.2).</li>
<li><a href="https://arxiv.org/abs/2603.29339">LongCat-AudioDiT: High-Fidelity Diffusion Text-to-Speech in the Waveform Latent Space</a>: the dimension and frame-rate sweeps with paired TTS models, the 3.5B scaling check, and Figures 3 and 4 (Sec. 5.3.2, pp. 11-12).</li>
<li><a href="https://arxiv.org/abs/2605.17085">Taming Audio VAEs via Target-KL Regularization</a>: target-KL as a specified information rate (Secs. 1-2), the rate-distortion curve (Fig. 1), the text-to-sound generation peak and the TTS prompt-copying caveat (Tables 2-3, Sec. 4).</li>
<li><a href="https://arxiv.org/abs/2509.22167">Semantic-VAE: Semantic-Alignment Latent Representation for Better Speech Synthesis</a>: reconstruction-equivalent VAEs with diverging F5-TTS results (Tables 1-2) and the cosine/L1/MSE loss-form ablation (Table 3).</li>
<li><a href="https://arxiv.org/abs/2412.08635">Multimodal Latent Language Modeling with Next-Token Diffusion</a> (LatentLM): the sigma-VAE recipe (Sec. 2.3), the frame-rate sweep (Tables 4-5), and the compression/width conversion ablation (Table 6).</li>
<li><a href="https://arxiv.org/abs/2605.16964">SemaVoice: Semantic-Aware Continuous Autoregressive Speech Synthesis</a>: the gradient-ratio-capped WavLM alignment (Eqs. 3-7) and the granularity sweep with and without alignment (Table 4).</li>
<li><a href="https://arxiv.org/abs/2508.19205">VibeVoice Technical Report</a>: the coupled-tokenizer collapse, the hybrid acoustic/semantic routing, and the semantic feedback path (Tables 5 and 7, Appendices B and D).</li>
<li><a href="https://arxiv.org/abs/2605.29209">The WER Trap</a>: the reconstruction/semantics competition measured from the discrete-token side (Tables 1-2).</li>
<li><a href="https://arxiv.org/abs/2604.09188">LatentFlowSR: High-Fidelity Audio Super-Resolution via Noise-Robust Latent Flow Matching</a>: the latent-noise-injection robustness ablation (Tables 4-5).</li>
<li><a href="https://arxiv.org/abs/2111.05011">RAVE: A Variational Autoencoder for Fast and High-Quality Neural Audio Synthesis</a>: two-stage training with the frozen encoder (Sec. 3.1.2), the SVD dimensionality estimate (Sec. 5.3, Fig. 2), and the unfrozen-encoder dimensionality inflation (Annex E, Fig. 8).</li>
<li><a href="https://arxiv.org/abs/2407.14358">Stable Audio Open</a>: the standard loss substrate, the 1e-4 KL weight, and the joint/frozen-phase compute split (Sec. 2).</li>
<li><a href="https://arxiv.org/abs/2301.12503">AudioLDM: Text-to-Audio Generation with Latent Diffusion Models</a>: the compression-level sweep where reconstruction holds and generation degrades (Tables 4 and 8).</li>
<li><a href="https://arxiv.org/abs/2412.16846">KALL-E: Autoregressive Speech Synthesis with Next-Distribution Prediction</a>: the deliberately high KL weight and its stated rationale (experimental setup and Table 1 discussion).</li>
<li><a href="https://arxiv.org/abs/2501.17578">Music2Latent2: Audio Compression with Summary Embeddings and Autoregressive Decoding</a>: unordered summary embeddings (Secs. 1-3).</li>
<li><a href="https://arxiv.org/abs/2602.23068">TADA: A Generative Framework for Speech Modeling via Text-Acoustic Dual Alignment</a>: text-token-aligned acoustic vectors (Sec. 3).</li>
<li><a href="https://arxiv.org/abs/2509.06926">Continuous Audio Language Models</a> (CALM): WavLM distillation over the full continuous speech latent, contrasted with Mimi's first-codebook distillation (Sec. 4.1).</li>
</ul>
<h2 id="related-diffio-posts">Related Diffio posts</h2>
<ul>
<li><a href="https://diffio.ai/blog/same-audio-encoder/">SAME Makes Generation Difficulty an Autoencoder Training Loss</a>: the single-system deep dive on shaping a latent with a jointly trained generation probe.</li>
<li><a href="https://diffio.ai/blog/vibevoice-next-token-diffusion/">VibeVoice: Frame Rate Is the Context Budget for Long-Form TTS</a>: the frame-rate budget arithmetic and the coupled-tokenizer capacity analysis.</li>
<li><a href="https://diffio.ai/blog/exposure-bias/">Exposure Bias: Corrupt the Context, Keep the Target Correct</a>: why autoregressive consumption makes latent variance an error-tolerance radius.</li>
<li><a href="https://diffio.ai/blog/flow-matching/">Flow Matching: Regress the Field the Sampler Integrates</a>: the training objective of most generators these latents are built for.</li>
<li><a href="https://diffio.ai/blog/tts-models-a-map-of-the-modern-speech-stack/">TTS Models: A Map Of The Modern Speech Stack</a>: where continuous-latent systems sit among the TTS architecture families.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>SAME Makes Generation Difficulty an Autoencoder Training Loss</title>
      <link>https://diffio.ai/blog/same-audio-encoder/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/same-audio-encoder/</guid>
      <pubDate>Fri, 12 Jun 2026 23:15:08 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>Audio</category>
      <category>ML</category>
      <description>Stability AI&apos;s SAME autoencoder, the latent space Stable Audio 3 generates in, makes generation difficulty part of the autoencoder&apos;s training loss.</description>
      <media:content url="https://diffio.ai/media/blog/same-audio-encoder/figure-1.webp" medium="image"/>
      <content:encoded><![CDATA[<p>SAME (Semantically-Aligned Music autoEncoder) is Stability AI's autoencoder for 44.1 kHz stereo music and general audio, and the frozen latent space that Stable Audio 3, the company's text-to-audio generator family, generates in. It compresses the waveform 4096x along the time axis into 256-dimensional latent frames at roughly 10.8 Hz, about twice the temporal compression of recent audio VAEs at four times their usual 64 channels. Two variants are released as open weights, SAME-L and the CPU-targeted SAME-S. The design problem at this operating point is producing a latent distribution that a flow-matching generator can learn, not reconstruction, and SAME attacks it directly: a small diffusion transformer trains jointly with the autoencoder on its latents, and its loss backpropagates into the encoder. In the paper's controlled ablation the auxiliary losses are load-bearing: the deterministic bottleneck they accompany is, alone, worse than the VAE it replaces, and with them it is better.</p>
<h2 id="a-4x-shorter-4x-wider-latent-at-the-same-scalar-rate">A 4x shorter, 4x wider latent at the same scalar rate</h2>
<p>Raising temporal downsampling shortens the sequence a downstream generator must model; at fixed latent dimension it also raises compression difficulty, so SAME widens the latent to compensate. At 10.8 Hz and 256 channels the latent carries about 2,760 floats per second, exactly the scalar rate of a conventional 1024x, 64-channel audio VAE: the budget is unchanged, reshaped into a 4x shorter, 4x wider sequence, because <a href="https://diffio.ai/blog/training-latent-audio-encoders/">sequence length is what a generator pays for</a> in compute and context. A 6-minute track is about 3,900 latent frames. Early latent-diffusion practice kept latent dimension small on the belief that wide latents are hard to generate in; SAME instead cites the representation-autoencoder (RAE) results from images, where large dimensions are tractable when the latent is semantically structured.</p>
<h2 id="patching-and-transformer-resampling-replace-strided-convolution">Patching and transformer resampling replace strided convolution</h2>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/same-audio-encoder/figure-1.webp" alt="SAME architecture: patching, encoder TRB, soft-normalization bottleneck, decoder TRB, unpatching, with reconstruction, adversarial, and auxiliary loss families." width="728" height="264" loading="lazy" decoding="async"></figure>

<p>SAME paper, Figure 1, p. 2. The auxiliary losses attach at the bottleneck, on the latent itself; the reconstruction and adversarial losses act on the decoded waveform.</p>
<p>There are no learned convolutions on the signal path. A parameter-free patching pretransform reshapes stereo waveform (B, 2, T) into (B, 2P, T/P) with P=256, so each embedding concatenates 256 left-channel and 256 right-channel samples: 256x downsampling as a pure reshape. The remaining 16x comes from a Transformer Resampling Block (TRB): the patch sequence is partitioned into non-overlapping 16-embedding segments, one learnable output embedding is appended to each, a transformer stack processes the interleaved sequence, and only the output embeddings are kept. The decoder TRB reverses the roles, pairing each latent with 16 learnable outputs.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/same-audio-encoder/figure-2.webp" alt="TRB encoder-mode interleaving: each segment of input embeddings gets one learnable query token; after the transformer stack, only the query tokens survive." width="570" height="325" loading="lazy" decoding="async"></figure>

<p>SAME paper, Figure 2, p. 3, shown with stride 2 for clarity; the released models use stride 16. Downsampling happens through attention into one learned token per segment, not through pooling or strided convolution.</p>
<p>In SAME-L the transformer layers use differential attention with RoPE and per-head QK-normalization, and the last 8 decoder layers swap their feed-forward activations for sin(pi x), a periodic basis for waveform detail; SAME-S drops both for CPU throughput. Both variants replace LayerNorm with Dynamic Tanh, a learnable tanh activation, avoiding normalization statistics that near-silent audio segments corrupt. Because training segments are a few seconds but inference must handle arbitrary lengths, full attention is unsuitable: SAME-L uses sliding-window attention, and SAME-S uses chunked attention whose chunk grid shifts by half a chunk midway through the stack, so first-half chunk boundaries fall in second-half chunk interiors and stop producing audible boundary artifacts; CPU inference libraries such as LiteRT do not support sliding windows. The all-transformer design buys speed: SAME-L runs at about twice the real-time factor (audio duration over encode-plus-decode wall clock) of smaller convolutional VAE baselines despite its 852M parameters, which the paper attributes to well-optimized transformer primitives plus the high compression ratio.</p>
<h2 id="soft-normalization-a-deterministic-bottleneck-that-pins-statistics">Soft-normalization: a deterministic bottleneck that pins statistics</h2>
<p>Where most continuous audio codecs put a VAE, SAME puts a deterministic normalization with no sampled posterior. The encoder output passes through a learnable per-channel affine transform, then is divided by a running standard deviation tracked by exponential moving average. A penalty of Gaussian KL form pushes means and variances toward zero and one along two axes independently, per channel over time and per timestep across channels, so neither individual channels nor individual frames drift to extreme statistics. Gaussian noise scaled by the running standard deviation is added to the latent (5e-2 during training, 1e-3 at inference), smoothing the latent manifold so the decoder tolerates the imperfect latents a downstream generator will hand it.</p>
<h2 id="l-diff-an-auxiliary-diffusion-model-trains-jointly-and-backpropagates-into-the-encoder">L_diff: an auxiliary diffusion model trains jointly and backpropagates into the encoder</h2>
<p>When building discrete tokenizers it is common to jointly train a small autoregressive prior whose loss backpropagates into the encoder, keeping the token distribution easy for that prior to model; for continuous latents under diffusion the paper calls the equivalent move rarer, citing a single recent precedent. SAME's version is the loss it calls L_diff: a 4-layer, 768-dimensional diffusion transformer trains on the latents with the standard straight-path <a href="https://diffio.ai/blog/flow-matching/">flow-matching objective</a>, noising z_t = (1-t)z + t*eps and regressing the velocity eps - z, with timesteps drawn from a truncated logistic-normal distribution. The auxiliary model warms up on detached latents; after warmup, its gradients flow into the encoder.</p>
<p>The auxiliary model is unconditional and exists only as a training signal. L_diff rises when the current latent distribution is hard for a flow-matching model to fit, so minimizing it with respect to the encoder pushes the latent toward geometries that are easier to generate in. An unconditional model this small is a coarse proxy for the text-conditioned generator that will actually use the latent; Stable Audio 3 pre-trains on the same frozen latents with the same straight-path velocity parameterization (its full objective adds minibatch optimal-transport coupling and an inpainting mask), so the encoder is shaped during its own training by a scaled-down version of the downstream generator.</p>
<h2 id="l-sem-and-l-con-linear-chroma-and-stereo-regressors-plus-a-contrastive-text-critic">L_sem and L_con: linear chroma and stereo regressors, plus a contrastive text critic</h2>
<p>Speech tokenizers usually buy semantic structure by aligning the latent to a self-supervised teacher such as WavLM. SAME aligns to no pretrained teacher; its semantic regression loss, L_sem, supervises with quantities computed from the signal itself. Single 1x1-convolution regressors predict three octave-banded chromagrams (per-frame energy by pitch class) and an interaural level difference (the per-band left/right log-magnitude gap that encodes the stereo image) directly from the latent; because the regressors are linear, pitch-class and spatial structure are pressured to stay linearly decodable rather than nonlinearly entangled. A separate contrastive loss, L_con, trains a critic to score whether a latent sequence, a wavelet decomposition of the audio, and a T5Gemma embedding of the paired text description come from the same input, with negatives formed by re-pairing audio and text within the batch and heavy masking of latent and feature positions to block shortcut cues. Like the auxiliary diffusion model, the critic warms up on detached latents before its gradients reach the encoder.</p>
<p>The pressure is mild: linear regressors and a warmed-up critic on a 256-dimensional latent. <a href="https://diffio.ai/blog/vibevoice-next-token-diffusion/">VibeVoice</a> found that the opposite setting, a hard transcript-decoding objective pushed through a 7.5 Hz, 64-dimensional speech latent, degraded reconstruction and collapsed speaker similarity.</p>
<h2 id="the-bottleneck-ablation-reconstruction-pinned-generation-decoupled">The bottleneck ablation: reconstruction pinned, generation decoupled</h2>
<p>The ablation holds everything fixed except the latent space: 50k autoencoder steps with the SAME-L backbone and no adversarial losses (removing GAN dynamics as a confounder), then a roughly 1.4B-parameter diffusion transformer (DiT) trained with flow matching on each latent space for 50k steps. VAE variants use a single, unswept KL weight (1e-4). Both budgets are short, so the comparison measures how readily a DiT fits each latent space at fixed compute, not converged quality.</p>
<div class="blog-table" role="region" aria-label="Table 1" tabindex="0"><table>
<thead>
<tr>
<th>Config</th>
<th>Compression / width</th>
<th>Bottleneck</th>
<th style="text-align:right">MELlog1p (recon)</th>
<th style="text-align:right">FAD-CLAP (gen)</th>
<th style="text-align:right">MuQEval (gen)</th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td>4096x / 256</td>
<td>VAE</td>
<td style="text-align:right">0.108</td>
<td style="text-align:right">0.651</td>
<td style="text-align:right">3.252</td>
</tr>
<tr>
<td>B</td>
<td>4096x / 256</td>
<td>soft-norm</td>
<td style="text-align:right">0.108</td>
<td style="text-align:right">1.061</td>
<td style="text-align:right">2.783</td>
</tr>
<tr>
<td>C</td>
<td>4096x / 256</td>
<td>soft-norm + L_diff</td>
<td style="text-align:right">0.103</td>
<td style="text-align:right">0.593</td>
<td style="text-align:right">3.340</td>
</tr>
<tr>
<td>D</td>
<td>4096x / 256</td>
<td>soft-norm + L_diff + L_sem/L_con</td>
<td style="text-align:right">0.109</td>
<td style="text-align:right"><strong>0.576</strong></td>
<td style="text-align:right"><strong>3.870</strong></td>
</tr>
<tr>
<td>E</td>
<td>1024x / 64</td>
<td>VAE</td>
<td style="text-align:right"><strong>0.098</strong></td>
<td style="text-align:right">0.724</td>
<td style="text-align:right">3.194</td>
</tr>
</tbody>
</table>
</div><p>SAME paper, Table 2, p. 8. MELlog1p is a multi-resolution log-mel reconstruction error (lower is better); FAD-CLAP is Fréchet audio distance in the embedding space of CLAP, a contrastive audio-text model, computed on the DiT's caption-conditioned outputs (lower is better); MuQEval is a model-based, reference-less musical quality score (higher is better).</p>
<p>Configs A and B reconstruct identically on the table's metric and generate very differently: FAD-CLAP 0.651 against 1.061. The statistics soft-normalization pins (means, variances, magnitudes) survived the swap, so whatever the DiT needed lives in distributional structure beyond those statistics, which a reconstruction metric cannot see. Adding the auxiliary diffusion model (config C) more than repays the simplification, beating the VAE baseline on all three metrics at no reconstruction cost; the table cannot say whether the gain is diffusion-specific shaping or a more generic smoothing pressure, and the untested VAE-plus-L_diff cell is what would separate the bottleneck's contribution from the auxiliary model's. Adding the semantic losses (config D) trades a little reconstruction for the best generation scores, with a MuQEval jump the paper reads as musical structure becoming easier to model; L_sem and L_con are added together, so their individual contributions are unknown.</p>
<p>Config E is the conventional 1024x/64 shape: the table's best reconstruction, and worse generation than A, C, and D. All generation evidence in the paper is this internal ablation; no generator is trained on a competitor's latent space. The same reconstruction/generation decoupling has been <a href="https://diffio.ai/blog/training-latent-audio-encoders/">measured independently across speech and audio systems</a>.</p>
<h2 id="full-scale-reconstruction-best-mel-error-and-listening-score-from-a-4x-shorter-sequence">Full-scale reconstruction: best mel error and listening score from a 4x shorter sequence</h2>
<p>The released models train in three stages on about 19,500 hours of licensed production audio (a 66/25/9 percent music, sound-effects, instrument-stems mix): an end-to-end stage with all losses, then two decoder-only stages with the encoder frozen, the second swapping in a transformer-based discriminator ensemble and synthetic linear frequency sweeps against aliasing.</p>
<div class="blog-table" role="region" aria-label="Table 2" tabindex="0"><table>
<thead>
<tr>
<th>Metric</th>
<th style="text-align:right">eps-ar-VAE</th>
<th style="text-align:right">ACE-Step 1.5</th>
<th style="text-align:right">SAO VAE</th>
<th style="text-align:right">CoDiCodec</th>
<th style="text-align:right">SAME-S</th>
<th style="text-align:right">SAME-L</th>
</tr>
</thead>
<tbody>
<tr>
<td>Compression</td>
<td style="text-align:right">1024x</td>
<td style="text-align:right">1920x</td>
<td style="text-align:right">2048x</td>
<td style="text-align:right">4096x</td>
<td style="text-align:right">4096x</td>
<td style="text-align:right">4096x</td>
</tr>
<tr>
<td>Latent dim</td>
<td style="text-align:right">64</td>
<td style="text-align:right">64</td>
<td style="text-align:right">64</td>
<td style="text-align:right">64</td>
<td style="text-align:right">256</td>
<td style="text-align:right">256</td>
</tr>
<tr>
<td>RTF</td>
<td style="text-align:right">325</td>
<td style="text-align:right">284</td>
<td style="text-align:right">300</td>
<td style="text-align:right">47</td>
<td style="text-align:right"><strong>2069</strong></td>
<td style="text-align:right">561</td>
</tr>
<tr>
<td>SI-SDR</td>
<td style="text-align:right"><strong>12.0</strong></td>
<td style="text-align:right">7.0</td>
<td style="text-align:right">6.2</td>
<td style="text-align:right">-0.3</td>
<td style="text-align:right">9.6</td>
<td style="text-align:right">11.9</td>
</tr>
<tr>
<td>STFTlog1p</td>
<td style="text-align:right"><strong>0.080</strong></td>
<td style="text-align:right">0.084</td>
<td style="text-align:right">0.092</td>
<td style="text-align:right">0.096</td>
<td style="text-align:right">0.088</td>
<td style="text-align:right">0.081</td>
</tr>
<tr>
<td>MELlog1p</td>
<td style="text-align:right">0.070</td>
<td style="text-align:right">0.069</td>
<td style="text-align:right">0.079</td>
<td style="text-align:right">0.096</td>
<td style="text-align:right">0.071</td>
<td style="text-align:right"><strong>0.057</strong></td>
</tr>
<tr>
<td>CCPC</td>
<td style="text-align:right"><strong>97.2</strong></td>
<td style="text-align:right">93.2</td>
<td style="text-align:right">92.2</td>
<td style="text-align:right">81.7</td>
<td style="text-align:right">95.5</td>
<td style="text-align:right">96.6</td>
</tr>
<tr>
<td>MUSHRA</td>
<td style="text-align:right">77.6</td>
<td style="text-align:right">76.5</td>
<td style="text-align:right">73.3</td>
<td style="text-align:right">n/a</td>
<td style="text-align:right">66.1</td>
<td style="text-align:right"><strong>82.2</strong></td>
</tr>
</tbody>
</table>
</div><p>SAME paper, Table 1, p. 8; baselines are recent open-weights continuous audio autoencoders. Objective metrics use 446 captioned music tracks from the Song Describer Dataset: SI-SDR is scale-invariant signal-to-distortion ratio in dB (higher is better), STFTlog1p a log-magnitude STFT distance (lower is better), CCPC cross-channel phase coherence for stereo-image fidelity (higher is better). RTF is averaged over 50 two-minute tracks, FP16 on one H100. MUSHRA is a 0-100 listening test on excerpts, here 36 valid trials from 12 participants after filtering, with the hidden reference (the clean original, rated blind) at 97.6; CoDiCodec was excluded from it.</p>
<p>SAME-L takes the best mel error and the best mean listening score (82.2 against 77.6 for the strongest baseline, with overlapping confidence intervals), while eps-ar-VAE, the conventional 1024x/64 shape, stays marginally ahead on SI-SDR, the magnitude-STFT distance, and stereo phase coherence. Every number here is music reconstruction; despite the sound-effects share in training, non-music quality is never evaluated in the paper.</p>
<p>SAME-S, the 108M CPU variant, matches the Stable Audio Open VAE on objective reconstruction at 6-7x the speed; its listening score is lower (66.1 to SAO's 73.3). Its distillation aligns student latents to the frozen SAME-L teacher's with an L1 loss and applies reconstruction and adversarial losses to cross-decoded outputs, student decoder on teacher latents and the reverse, so the two models share one latent space with interchangeable latents. That is what lets Stable Audio 3 train its small model on SAME-S and its larger models on SAME-L.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://arxiv.org/abs/2605.18613">SAME: A Semantically-Aligned Music Autoencoder</a>: all architecture and training details (Secs. 2-4), the bottleneck ablation (Table 2, Sec. 5.1), reconstruction and MUSHRA results (Table 1, Secs. 5.3-5.4), Figures 1 and 2.</li>
<li><a href="https://arxiv.org/abs/2605.17991">Stable Audio 3</a>: the generator trained on SAME latents; model tiers on SAME-S/SAME-L and the flow-matching pre-training objective (Secs. 2.1, 3.2).</li>
<li><a href="https://arxiv.org/abs/2510.11690">Diffusion Transformers with Representation Autoencoders</a>: the image-domain wide-latent argument SAME cites for choosing d=256.</li>
<li><a href="https://stability-ai.github.io/SAME">SAME open weights and audio examples</a></li>
</ul>
<h2 id="related-diffio-posts">Related Diffio Posts</h2>
<ul>
<li><a href="https://diffio.ai/blog/flow-matching/">Flow Matching: Regress the Field the Sampler Integrates</a></li>
<li><a href="https://diffio.ai/blog/training-latent-audio-encoders/">Latent Audio Encoders: Reconstruction Sets the Ceiling, Not the Ranking</a></li>
<li><a href="https://diffio.ai/blog/vibevoice-next-token-diffusion/">VibeVoice: Frame Rate Is the Context Budget for Long-Form TTS</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Flow Matching: Regress the Field the Sampler Integrates</title>
      <link>https://diffio.ai/blog/flow-matching/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/flow-matching/</guid>
      <pubDate>Fri, 12 Jun 2026 23:11:35 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>ML</category>
      <description>Flow matching trains a generator by regressing the velocity field its ODE sampler integrates: conditional targets, the optimal-transport path, and why it trains robustly.</description>
      <media:content url="https://diffio.ai/media/blog/flow-matching/figure-4.webp" medium="image"/>
      <content:encoded><![CDATA[<p>Flow matching trains a continuous normalizing flow: a generative model that parameterizes a time-dependent vector field v_theta(x, t) with a neural network and produces samples by drawing noise x_0 from N(0, I) and integrating the ODE dx/dt = v_theta(x, t) from t = 0 to t = 1. The training objective, introduced by Lipman et al., is plain least squares: choose a probability path p_t that interpolates between the noise distribution and the data distribution, and regress v_theta onto the velocity field u_t that generates that path, meaning that transporting samples along u_t reproduces p_t at every t.</p>
<pre class="blog-code"><code>L_FM = E_{t, x ~ p_t} || v_theta(x, t) - u_t(x) ||^2
</code></pre>
<p>Two properties make this the default way to train continuous generators. The regression target is the same field the ODE solver integrates at sampling time, and its scale can be made uniform over t, which together make training markedly more robust than score matching at identical architecture and compute. And the path is a free choice rather than the byproduct of a diffusion process, which admits the straight optimal-transport path: a velocity target that is constant in time during training, and, empirically, the same sample quality at a fraction of the solver steps.</p>
<h2 id="per-example-velocity-targets-optimize-the-intractable-marginal-objective">Per-example velocity targets optimize the intractable marginal objective</h2>
<p>The loss above cannot be computed as written: for any path that ends at the data distribution, u_t depends on the unknown data density, so there is nothing to regress onto. Conditional flow matching (CFM) is the construction that fixes this. Condition on one data sample x_1 and define a per-example Gaussian path p_t(x | x_1) that starts at standard noise at t = 0 and ends at a concentrated Gaussian N(x_1, sigma_min^2 I) around the sample at t = 1, with sigma_min a small constant, typically 1e-5. Marginalizing over the data distribution q(x_1) yields a path whose endpoint approximates the data. Writing x_t for a sample on the path at time t, the field that generates this marginal path is the average of the conditional fields over every x_1 that could have produced the observed x_t, weighted by the posterior:</p>
<pre class="blog-code"><code>u_t(x) = E[ u_t(x | x_1) | x_t = x ]
</code></pre>
<p>The CFM loss regresses the network onto the per-example targets instead:</p>
<pre class="blog-code"><code>L_CFM = E_{t, x_1 ~ q, x ~ p_t(x | x_1)} || v_theta(x, t) - u_t(x | x_1) ||^2
</code></pre>
<p>This works because of what least squares does with targets that vary given the input: the minimizing function is their conditional expectation, which by the equation above is exactly the marginal field. Lipman et al. prove the two statements that make this exact: the marginal field assembled from conditional fields generates the marginal path (Theorem 1), and L_CFM and L_FM differ by a constant independent of theta, so their gradients are identical (Theorem 2). Training on closed-form per-example targets therefore optimizes the intractable marginal objective exactly, in expectation. The construction is the same one denoising score matching uses for diffusion models, generalized from scores to arbitrary velocity fields.</p>
<p>For a Gaussian conditional path N(mu_t(x_1), sigma_t(x_1)^2 I), the affine map psi_t(x) = sigma_t(x_1) x + mu_t(x_1) pushes standard noise onto the path, and Theorem 3 derives the unique field whose flow is that map (primes denote time derivatives):</p>
<pre class="blog-code"><code>u_t(x | x_1) = (sigma_t'(x_1) / sigma_t(x_1)) (x - mu_t(x_1)) + mu_t'(x_1)
</code></pre>
<p>A training step is then: sample t uniformly on [0, 1], a data sample x_1, and noise x_0; form x_t = sigma_t(x_1) x_0 + mu_t(x_1); take a gradient step on the squared error against the closed-form target. No ODE is solved during training. Papers do not share a time convention (the flow-matching paper puts noise at t = 0 and data at t = 1; much of the diffusion literature and several flow-matching systems run the other way), and velocity signs flip with the convention, so equations cannot be copied across papers without checking it.</p>
<h2 id="the-path-is-a-free-choice-diffusion-paths-are-special-cases-the-ot-path-is-the-alternative">The path is a free choice: diffusion paths are special cases, the OT path is the alternative</h2>
<p>The other half of the framework is that mu_t and sigma_t are free choices. Setting them to the schedule of the variance-preserving (VP) diffusion process recovers that diffusion path exactly, and the conditional field Theorem 3 assigns it coincides with the drift of score-based diffusion's probability-flow ODE (Appendix D of the paper). Deterministic diffusion sampling and flow-matching sampling are therefore the same operation, an ODE solve, and on the same path the ideal field is identical. What distinguishes the methods is the regressed quantity, its loss weighting, and which paths the construction can express: diffusion paths must arise from a forward noising process, and one inherited consequence is that they never reach the noise distribution in finite time, so the prior at t = 0 is only ever approximated.</p>
<p>The optimal-transport (OT) path makes mean and standard deviation linear in time:</p>
<pre class="blog-code"><code>mu_t(x_1) = t x_1          sigma_t(x_1) = 1 - (1 - sigma_min) t
x_t = (1 - (1 - sigma_min) t) x_0 + t x_1
u_t(x_t | x_1) = x_1 - (1 - sigma_min) x_0
</code></pre>
<p>The conditional flow is the optimal-transport displacement map between the two endpoint Gaussians: each conditional sample moves along a straight line at constant speed, the minimal-cost interpolation under squared distance. Along its own trajectory, the regression target is the constant x_1 - (1 - sigma_min) x_0; as a field over all of space, u_t factors as g(t) h(x | x_1), so its direction at any point never changes and only its scale depends on t, a simpler regression target than one that rotates as t advances. And conditional sampling trajectories stay straight, where diffusion-path trajectories can overshoot the endpoint and backtrack.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/flow-matching/figure-2.webp" alt="Diffusion conditional score function versus OT conditional vector field over time" width="1118" height="190" loading="lazy" decoding="async"></figure>

<p>Source: Flow Matching for Generative Modeling, Figure 2, p. 6. What to see: across the four time panels, the diffusion path's conditional score (left) changes direction and magnitude as t advances, while the OT path's conditional vector field (right) keeps a constant direction.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/flow-matching/figure-3.webp" alt="Curved diffusion sampling trajectories versus straight OT trajectories" width="332" height="188" loading="lazy" decoding="async"></figure>

<p>Source: Flow Matching for Generative Modeling, Figure 3, p. 6. What to see: trajectories of the diffusion conditional field (left) curve and backtrack; the OT conditional trajectories (right) run straight to the same endpoints.</p>
<p>Conditional optimality does not make the marginal flow an optimal-transport map. Where conditional paths cross, the marginal field is a posterior average of disagreeing conditional velocities, so marginal trajectories still curve; the paper claims only that the marginal field stays relatively simple, and closing this gap is what rectified flow's reflow procedure and minibatch OT coupling target.</p>
<h2 id="the-score-targets-scale-diverges-the-velocity-loss-measures-the-field-the-solver-integrates">The score target's scale diverges; the velocity loss measures the field the solver integrates</h2>
<p>Two mechanisms separate the objectives. The first is target scale. For a Gaussian conditional path, the score (the gradient of the log density, which diffusion training regresses onto) is -(x - mu_t) / sigma_t^2 = -x_0 / sigma_t, with expected squared norm d / sigma_t^2 in d dimensions. As the path concentrates onto the data (sigma_t toward 0), the target's scale diverges, so an unweighted score regression is dominated by the times nearest the data, and every score-matching recipe inserts a per-time loss weight lambda(t) to compensate. The weightings are heuristics: Grad-TTS, a score-based TTS model, weights its per-time losses by lambda_t and states the rule it follows, that weights should be proportional to 1 / E||target||^2. The OT velocity target needs no weighting; its distribution is the same at every t. The same arithmetic says why a path ending in a near-point mass is trainable under velocity regression and pathological as a score target, whose norm at the endpoint scales as 1 / sigma_min.</p>
<p>The second is what the loss measures. The CFM loss is the squared error of the field the ODE solver will integrate, weighted uniformly over t. Score matching trains a score network, and the sampler then assembles the drift from it, with the score entering scaled by beta(1-t) / 2 on the VP path (beta is the VP noise schedule; the 1-t argument is the time-convention flip noted above). Converting squared score error into squared drift error therefore means weighting by (beta(1-t) / 2)^2, and neither standard recipe uses that weight: original score matching takes lambda(t) = sigma_t^2 and ScoreFlow takes beta(1-t). The trained loss and the error the solver accumulates are different time-weightings of the same error, so one can be low while the other is not.</p>
<p>Grad-TTS documents what that looks like: its diffusion loss converges early and then barely moves while output quality keeps improving across 1.7 million training iterations, and the authors' explanation is that score estimates inaccurate on a small subset of t barely move the weighted loss but break the ODE solve whenever the solver lands there. That much is true of any time-integrated loss, flow matching's included; what flow matching removes is the systematic mismatch. With an adaptive solver, the number of function evaluations (NFE, how many times the solver calls the network) needed to sample a score-matching model changes drastically over training, while the flow-matching models' sampling cost stays near its final value from the first epochs.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/flow-matching/figure-10.webp" alt="Adaptive-solver NFE over training for score matching and flow matching" width="577" height="352" loading="lazy" decoding="async"></figure>

<p>Source: Flow Matching for Generative Modeling, Figure 10, p. 21 (CIFAR-10, dopri5 solver at tolerance 1e-5). What to see: the score-matching curve (green) is the unstable one, spiking past 1,000 evaluations and drifting for hundreds of epochs, while both flow-matching curves stay near their final cost, the OT model (orange) cheapest.</p>
<h2 id="two-ablations-separate-the-objective-from-the-path">Two ablations separate the objective from the path</h2>
<p>The flow-matching paper trains the same U-Net architecture with the same hyperparameters under five configurations (three diffusion losses, flow matching on two paths), with the baselines allowed more training iterations to converge.</p>
<div class="blog-table" role="region" aria-label="Table 1" tabindex="0"><table>
<thead>
<tr>
<th>Loss</th>
<th style="text-align:right">ImageNet-64 FID</th>
<th style="text-align:right">ImageNet-64 NFE</th>
<th style="text-align:right">CIFAR-10 FID</th>
</tr>
</thead>
<tbody>
<tr>
<td>DDPM (noise matching)</td>
<td style="text-align:right">17.36</td>
<td style="text-align:right">264</td>
<td style="text-align:right">7.48</td>
</tr>
<tr>
<td>Score matching</td>
<td style="text-align:right">19.74</td>
<td style="text-align:right">441</td>
<td style="text-align:right">19.94</td>
</tr>
<tr>
<td>ScoreFlow</td>
<td style="text-align:right">24.95</td>
<td style="text-align:right">601</td>
<td style="text-align:right">20.78</td>
</tr>
<tr>
<td>FM, diffusion path</td>
<td style="text-align:right">16.88</td>
<td style="text-align:right">187</td>
<td style="text-align:right">8.06</td>
</tr>
<tr>
<td>FM, OT path</td>
<td style="text-align:right">14.45</td>
<td style="text-align:right">138</td>
<td style="text-align:right">6.35</td>
</tr>
</tbody>
</table>
</div><p>Source: Flow Matching for Generative Modeling, Table 1, p. 8. FID is Frechet Inception Distance, lower is better; the NFE column is the adaptive solver's evaluation count averaged over 50K samples. DDPM regresses the added noise; ScoreFlow is score matching with a likelihood-motivated weighting. None of these unconditional models is tuned for absolute state of the art (the paper suggests the architecture was not optimized for CIFAR-10 at all), so read the columns as controlled within-table contrasts.</p>
<p>FM with the OT path is best on every dataset and metric in the paper's full Table 1, test likelihood included. The within-path contrast carries the objective claim: holding the VP diffusion path fixed, the two score parameterizations degrade sharply on CIFAR-10 while the two constant-scale regression targets, DDPM's noise (a standard Gaussian at every t) and FM's velocity, stay stable. A well-conditioned target buys the stability whether it is noise or velocity; what noise matching does not buy is cheap integration, a roughly 40% higher adaptive-solver cost than FM on the identical path. The objective's advantage is robustness rather than uniform dominance: score matching edges FM-on-diffusion on ImageNet-32 (FID 5.68 against 6.37, with a lower adaptive NFE as well). Training cost moves the same direction, though only across papers: the ImageNet-128 FM-OT model reached its reported FID on a third fewer total training images than the ADM baseline of Dhariwal and Nichol, with a model 25% larger and otherwise differently tuned.</p>
<p>Voicebox replicates the factorization in speech: one model, one dataset, trained three ways (FM with the OT path, FM with the VP diffusion path, score matching with the same diffusion path), evaluated as zero-shot TTS by WER, the word error rate of an ASR model transcribing the synthesized speech. The comparison runs in a reduced setup with a lowered learning rate so that all three objectives converge.</p>
<div class="blog-table" role="region" aria-label="Table 2" tabindex="0"><table>
<thead>
<tr>
<th>Objective</th>
<th style="text-align:right">WER, 50K updates (NFE 32)</th>
<th style="text-align:right">WER, 150K updates (NFE 32)</th>
<th style="text-align:right">WER, 150K updates (NFE 4)</th>
</tr>
</thead>
<tbody>
<tr>
<td>FM, OT path</td>
<td style="text-align:right">2.5</td>
<td style="text-align:right">2.1</td>
<td style="text-align:right">2.4</td>
</tr>
<tr>
<td>FM, diffusion path</td>
<td style="text-align:right">76.0</td>
<td style="text-align:right">2.6</td>
<td style="text-align:right">11.5</td>
</tr>
<tr>
<td>SM, diffusion path</td>
<td style="text-align:right">73.3</td>
<td style="text-align:right">5.1</td>
<td style="text-align:right">94.5</td>
</tr>
</tbody>
</table>
</div><p>Source: Voicebox, the objective-comparison tables in Section 5.8 (Tables 8 and 9 in the arXiv version). For scale, ground-truth recordings score WER 2.2 on this evaluation.</p>
<p>At a 50K-update budget only the OT path yields a usable model; both diffusion-path variants still fail to produce intelligible speech. At 150K updates the objective swap on the identical path cuts WER roughly in half, and the path swap closes the rest of the gap to ground truth. In the low-NFE column both factors matter, but only the OT model stays usable at four evaluations, and score matching is still degraded at the table's maximum of 32.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/flow-matching/figure-4.webp" alt="Checkerboard density evolution and low-NFE sampling for three objectives" width="1113" height="331" loading="lazy" decoding="async"></figure>

<p>Source: Flow Matching for Generative Modeling, Figure 4, p. 7. What to see: on the left, the 2D checkerboard pattern emerges much earlier along the flow for FM with OT (bottom row) than for the diffusion-path models above it; on the right, FM-OT samples remain clean at NFE 4 to 20 while the other objectives' samples degrade.</p>
<h2 id="sampling-the-nfe-is-chosen-at-inference-time-and-the-path-shifts-the-quality-nfe-trade-off">Sampling: the NFE is chosen at inference time, and the path shifts the quality/NFE trade-off</h2>
<p>Any off-the-shelf ODE solver turns a noise draw into a sample by integrating the learned field, and with a fixed-step solver the NFE is proportional to the step count, so the quality/latency operating point is chosen at inference time, per run, with no retraining. Deterministic diffusion samplers share that property; what the path changes is where the trade-off sits. On ImageNet-32 with the same fixed-step solvers, the FM-OT model reaches the same numerical error as the diffusion-path models with roughly 60% of the function evaluations and keeps usable FID at very low NFE, and Voicebox's NFE=4 column above is the speech version of the same gap.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/flow-matching/figure-7.webp" alt="ODE solution error and FID versus NFE for the three objectives" width="1055" height="207" loading="lazy" decoding="async"></figure>

<p>Source: Flow Matching for Generative Modeling, Figure 7, p. 9 (ImageNet-32, fixed-step solvers). What to see: in the left panel the FM-OT model (green) reaches a given numerical error at the fewest evaluations, and in the FID panels FM-OT sits near its converged quality at NFEs where score matching has not yet left the high-FID regime.</p>
<p>Classifier-free guidance transfers unchanged: train with condition dropout, then mix the conditional and unconditional fields at every solver step, which doubles the network evaluations for a fixed NFE. The mechanism, a guidance scale acting as an exponent on an implicit classifier, is covered in <a href="https://diffio.ai/blog/classifier-free-guidance/">Classifier-Free Guidance: The Guidance Scale Is an Exponent on an Implicit Classifier</a>. In audio the objective appears at more than one interface: Voicebox generates whole mel-spectrogram sequences with it, and a growing family generates the latents of a pretrained audio autoencoder, the setting whose encoder-side requirements are covered in <a href="https://diffio.ai/blog/training-latent-audio-encoders/">Latent Audio Encoders: Reconstruction Sets the Ceiling, Not the Ranking</a>. The objective also appears inside autoencoder training itself: <a href="https://diffio.ai/blog/same-audio-encoder/">SAME Makes Generation Difficulty an Autoencoder Training Loss</a> trains a small flow-matching model jointly with a music autoencoder and backpropagates its loss into the encoder, shaping the latent space the downstream generator inherits.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://arxiv.org/abs/2210.02747">Lipman et al., Flow Matching for Generative Modeling (arXiv:2210.02747)</a>: FM and CFM objectives with Theorems 1-3, Sections 2-4 (objective section); Gaussian path family, VP and OT instances, and Figures 2 and 3, Section 4.1 (path section, both figures reproduced); probability-flow coincidence, Appendix D, and the score/noise-matching loss definitions with their weightings and the sampling drift, Appendix E (path and training-gap sections); Table 1, Figure 4, Figure 10, and the ImageNet-128 training budget, Section 6.1 (ablation section; Figure 4 reproduced as header and body figure, Figure 10 reproduced); the 60% NFE result and Figure 7, Section 6.2 (sampling section, figure reproduced).</li>
<li><a href="https://arxiv.org/abs/2306.15687">Le et al., Voicebox: Text-Guided Multilingual Universal Speech Generation at Scale (arXiv:2306.15687)</a>: flow matching with the OT path and classifier-free guidance for vector fields, Sections 3.1-3.5; the objective and path ablations, Section 5.8, Tables 8 and 9, with the reduced training setup in Appendix B.1 and the full grid in Table B4 (ablation and sampling sections); the ground-truth WER reference point in the English zero-shot TTS results table of Section 5.1.</li>
<li><a href="https://arxiv.org/abs/2105.06337">Popov et al., Grad-TTS: A Diffusion Probabilistic Model for Text-to-Speech (arXiv:2105.06337)</a>: the score-matching loss and its lambda_t weighting with the stated weighting heuristic, Sections 2-3.3; the loss-plateau observation and the small-subset-of-t explanation, Section 4 and Figure 3 (training-gap section).</li>
</ul>
<h2 id="related-diffio-posts">Related Diffio Posts</h2>
<ul>
<li><a href="https://diffio.ai/blog/classifier-free-guidance/">Classifier-Free Guidance: The Guidance Scale Is an Exponent on an Implicit Classifier</a>: the sampling-time guidance mechanism flow-matching systems inherit.</li>
<li><a href="https://diffio.ai/blog/training-latent-audio-encoders/">Latent Audio Encoders: Reconstruction Sets the Ceiling, Not the Ranking</a>: what the latent space must provide when flow matching generates autoencoder latents instead of waveforms or spectrograms.</li>
<li><a href="https://diffio.ai/blog/same-audio-encoder/">SAME Makes Generation Difficulty an Autoencoder Training Loss</a>: the flow-matching objective used as an auxiliary loss inside an audio autoencoder, shaping the latent space its generator trains on.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Mega-ASR: Group-Relative RL Needs a Reward That Ranks Failed Transcripts</title>
      <link>https://diffio.ai/blog/mega-asr-reward-design/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/mega-asr-reward-design/</guid>
      <pubDate>Fri, 12 Jun 2026 22:00:42 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>Audio</category>
      <category>ML</category>
      <description>Mega-ASR makes Qwen3-ASR robust to degraded, in-the-wild audio with group-relative RL. Why that recipe needs a reward that can rank failed transcripts.</description>
      <media:content url="https://diffio.ai/media/blog/mega-asr-reward-design/figure-4.webp" medium="image"/>
      <content:encoded><![CDATA[<p>Mega-ASR is a robustness recipe for Qwen3-ASR-1.7B, an audio-LLM ASR system in which an acoustic encoder feeds an aligner (an adapter that maps encoder outputs into the LLM's embedding space) and the LLM decodes the transcript. The recipe has four parts: Voices-in-the-Wild-2M, a 2.4M-clip simulated corpus of compound acoustic degradation; Acoustic-to-Semantic progressive supervised fine-tuning (A2S-SFT), a three-phase LoRA schedule; an RL stage, Dual-Granularity WER-Gated Policy Optimization (DG-WGPO), built on the group-relative method DAPO; and an inference-time router that activates the robust weights only when the input is degraded. The target is what the authors call ASR-in-the-wild²: speech that is both in the wild and degraded by several effects at once, where word error rate (WER: substitutions, insertions, and deletions divided by reference word count) rises past 30%. On the three standard robustness benchmarks, the recipe cuts the backbone's average WER from 7.93 to 6.70.</p>
<p>The finding with the widest scope is in the RL stage: running DAPO with the standard <code>1 - WER</code> reward moved little over the supervised checkpoint, and the gains appeared after the reward was rebuilt to score how a transcript fails, not how many words it gets wrong. The rest of the pipeline encodes the same idea at the data level.</p>
<h2 id="severe-degradation-changes-the-failure-mode-not-just-the-error-rate">Severe degradation changes the failure mode, not just the error rate</h2>
<p>The paper's organizing observation is a regime boundary. At WER at or below roughly 30%, recognition errors are predominantly word-level confusions: acoustically plausible substitutions a reward can grade locally. Beyond that threshold, the paper reports, the dominant failure switches to sentence-level breakdowns: fluent hallucinations, large omissions, and empty outputs. The model has lost acoustic grounding, and the language model fills the gap with whatever its prior finds likely. The evidence for the boundary is observational (training-time inspection and case studies), and the same 30/50/70 numbers reappear as the pipeline's hyperparameters, so read the specific threshold as the paper's chosen operating point rather than a measured phase transition.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/mega-asr-reward-design/figure-6.webp" alt="Mega-ASR Figure 6: case study comparing Qwen3-ASR, Gemini-3.1-Pro, and Mega-ASR transcripts under severe degradation" width="1089" height="1264" loading="lazy" decoding="async"></figure>

<p>Mega-ASR paper, Figure 6, page 10. The far-field panel shows the failure-mode shift: on the same heavily degraded clip, Qwen3-ASR returns an empty transcript (WER 100%), Gemini-3.1-Pro fabricates fluent content unrelated to the audio (WER 86.1%), and Mega-ASR recovers the reference exactly. Neither baseline failure is a word confusion.</p>
<p>Every pipeline stage is built around this boundary. The dataset discards generated samples above the 70% cap, which the authors observed destabilize training. The first supervised phase orders its curriculum by the same thresholds. The RL split is enriched in medium- and high-WER examples, selected by each example's stored base-model WER. And the RL reward switches its emphasis at a WER gate.</p>
<h2 id="voices-in-the-wild-2m-seven-effects-compose-into-54-scenarios-under-one-severity-value">Voices-in-the-Wild-2M: seven effects compose into 54 scenarios under one severity value</h2>
<p>Stacking degradations is standard augmentation practice; what the released robust-ASR corpora the paper compares against lack is compositional difficulty, since they mostly cover one or two isolated conditions that the backbone already transcribes at 4-10% WER. The corpus is built as a three-level compositional hierarchy. Eight primitive signal operators (additive noise, echo delay, reverberation, nonlinear distortion, resampling, spectral filtering, loudness transformation, frame-level stutter) compose into seven atomic effects. Each atomic effect is an ordered operator chain with one dominant operator plus the secondary artifacts that physically co-occur with it: far-field is reverb, then low-pass filtering, then a loudness drop, because a distant speaker is simultaneously more reverberant, spectrally attenuated, and quieter; obstructed (speech through a wall, door, or mask) is dominated by repeated low-pass filtering. The clean speech underneath comes from public English and Mandarin corpora (LibriSpeech and Common Voice; WenetSpeech and AISHELL-1), with noise drawn from MUSAN and similar collections.</p>
<p>The atomic effects then compose into 54 compound scenarios under an anchor-modifier rule. Far-field, echo&amp;reverb, and obstructed define mutually exclusive propagation geometries and never combine with each other; noise, recording coloration, electronic distortion, and transmission dropout are modifiers that attach to any geometry. The enumeration is rule-based but not exhaustive: at the three-effect level only 9 of the 18 possible anchor-plus-two-modifier combinations are kept, and the introduction credits an &quot;agentic check&quot; for physical plausibility that the paper never specifies.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/mega-asr-reward-design/figure-2.webp" alt="Mega-ASR Figure 2: Voices-in-the-Wild-2M overview, seven meta-scenarios expanded into 54 hybrid scenarios" width="1088" height="538" loading="lazy" decoding="async"></figure>

<p>Mega-ASR paper, Figure 2, page 4. The seven atomic-effect tiles expand into the 54 hybrid scenarios; the header panel carries the corpus scale (2.4M clips, 11K hours).</p>
<p>Each sample draws a single severity value shared by every active operator; every operator parameter declares which direction is harder and is set from that one value. Independent per-effect sampling would produce incoherent samples (severe reverberation over an otherwise pristine recording); the shared value makes a hard sample hard everywhere. Linear severity sampling beat hard-biased, easy-biased, and middle-biased alternatives in the paper's 50K-sample pilot runs. The per-effect parameters were tuned against real recordings via SFT validation, though the paper names neither the calibration set nor the fit objective, and its own difficulty numbers conflict: the comparison table puts the backbone at 18.42 average WER on the corpus, the text says 35%. Either figure is well above the corpora it compares against.</p>
<h2 id="a2s-sft-ground-the-encoder-and-aligner-before-the-language-prior-adapts">A2S-SFT: ground the encoder and aligner before the language prior adapts</h2>
<p>Recognition under heavy degradation has two coupled bottlenecks: extracting reliable acoustic evidence from a corrupted signal, and using the language model's prior to reconstruct the transcript when that evidence is only partially reliable. The supervised stage trains them in that order, with rank-8 LoRA throughout. Phase one updates only the encoder and aligner, on a curriculum that expands through the three WER thresholds in order. Phase two freezes the acoustic side and adapts the LLM on the full targeted split, no curriculum, activating semantic recovery. Phase three updates everything jointly, with the acoustic side on a lower learning rate to preserve phase one. The schedule itself is the standard align-then-joint recipe for multimodal LLMs; the additions are the phase-one curriculum and a measurement of how much the staging is worth.</p>
<p>The order matters for the same reason the high-WER failures look the way they do. In the authors' preliminary variants, joint training from the start was unstable on medium- and high-WER samples because the LLM adapted to acoustic representations that were still unreliable, and adapting the LLM before the acoustic warm start made the model more prone to relying on its language prior. A prior that learns to override a noisy encoder during training is the same mechanism that produces fluent off-audio transcripts at inference. In the ablation table below, unstaged SFT on the same data recovers only about half of the staged schedule's improvement.</p>
<h2 id="dg-wgpo-a-reward-built-to-keep-ranking-transcripts-after-the-wer-reward-saturates">DG-WGPO: a reward built to keep ranking transcripts after the WER reward saturates</h2>
<p>The RL stage samples a group of transcript candidates per audio clip and updates with GRPO-style group-relative advantages under the DAPO loss: each candidate's advantage is its reward measured against the group's, so the learning signal lives entirely in within-group reward differences. Our <a href="https://diffio.ai/blog/grpo-dapo/">GRPO/DAPO post</a> covers the general mechanism and DAPO's changes to GRPO. A reward that stops discriminating between candidates therefore stops producing gradient, and the paper reports exactly that failure for the plain WER reward: under heavy degradation it saturates, collapsing reward dispersion within the rollout group precisely where the policy most needs signal. Sequence-discriminative training against expected word error (MWER, sMBR) is decades-old ASR practice and works in its regime; what breaks here is the group-relative estimator.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/mega-asr-reward-design/figure-4.webp" alt="Mega-ASR Figure 4: DG-WGPO framework with A2S-SFT initialization, rollout group, and gated dual-granularity reward" width="1088" height="420" loading="lazy" decoding="async"></figure>

<p>Mega-ASR paper, Figure 4, page 5. Left: the three A2S-SFT phases. Center: the policy's rollout group, scored by a static rule-based reward and by the dual-granularity dynamic reward whose two terms a WER gate mixes. Right: the combined rewards become group-relative advantages.</p>
<p>The redesigned reward keeps <code>R_static = R_rep * (1 - WER)</code> as an anchor, where the repetition gate <code>R_rep</code> zeroes any rollout that collapses into repeated n-grams. On top of it sits a dynamic reward with two terms. The token-level term <code>R_fine</code> splits substitutions by character-level edit similarity: a near-miss with similarity at or above 0.5 is discounted to 0.4 of an error, while insertions and deletions always count fully, since unlike a near-miss substitution neither is an acoustic confusion. The sentence-level term <code>R_struc</code> ignores token identity and scores structural preservation of the reference: half from longest-common-subsequence recall, half from a length-integrity term that reaches zero when the hypothesis is empty or runs away. <code>R_struc</code> is what separates an empty output from a truncated-but-faithful one from a fluent fabrication, distinctions that all live in the saturated tail of <code>1 - WER</code>.</p>
<p>A WER gate, evaluated on each rollout's own WER against the reference, swaps the two weights at a threshold: below it the token-level term dominates the mix, above it the sentence-level term does, and the final reward weights the dynamic pair over the static anchor. The gate threshold is 0.3 in the main text and its sweep but 0.5 in the appendix and in the framework figure above, and the two halves of the paper also disagree on the RL learning rate, the rollout count (16 versus 12), and the SFT learning-rate scheme, so the released numbers pin down the design but not the exact operating point.</p>
<h2 id="the-ablation-reward-design-not-the-optimizer-carried-the-rl-stage">The ablation: reward design, not the optimizer, carried the RL stage</h2>
<p>The table below is measured on the medium- and high-difficulty subsets of VOiCES (a far-field room-microphone corpus) and NOIZEUS (noisy speech at controlled signal-to-noise ratios).</p>
<div class="blog-table" role="region" aria-label="Table 1" tabindex="0"><table>
<thead>
<tr>
<th>Variant</th>
<th style="text-align:right">VOiCES WER</th>
<th style="text-align:right">NOIZEUS WER</th>
</tr>
</thead>
<tbody>
<tr>
<td>Qwen3-ASR baseline</td>
<td style="text-align:right">8.94</td>
<td style="text-align:right">9.45</td>
</tr>
<tr>
<td>SFT without staging</td>
<td style="text-align:right">8.31</td>
<td style="text-align:right">8.79</td>
</tr>
<tr>
<td>Mega-ASR-Base (staged A2S-SFT)</td>
<td style="text-align:right">7.59</td>
<td style="text-align:right">8.12</td>
</tr>
<tr>
<td>+ vanilla GRPO, <code>1 - WER</code> reward only</td>
<td style="text-align:right">7.73</td>
<td style="text-align:right">8.11</td>
</tr>
<tr>
<td>+ vanilla DAPO, <code>1 - WER</code> reward only</td>
<td style="text-align:right">7.62</td>
<td style="text-align:right">7.98</td>
</tr>
<tr>
<td>+ DG-WGPO without <code>R_rep</code></td>
<td style="text-align:right">7.46</td>
<td style="text-align:right">7.73</td>
</tr>
<tr>
<td>+ DG-WGPO without <code>R_fine</code></td>
<td style="text-align:right">7.45</td>
<td style="text-align:right">7.71</td>
</tr>
<tr>
<td>+ DG-WGPO without <code>R_struc</code></td>
<td style="text-align:right">7.54</td>
<td style="text-align:right">7.85</td>
</tr>
<tr>
<td>+ DG-WGPO without gated fusion</td>
<td style="text-align:right">7.41</td>
<td style="text-align:right">7.68</td>
</tr>
<tr>
<td>Mega-ASR (full DG-WGPO)</td>
<td style="text-align:right">7.35</td>
<td style="text-align:right">7.64</td>
</tr>
</tbody>
</table>
</div><p>Mega-ASR paper, Table 5, page 8. WER (%), lower is better, on the VOiCES and NOIZEUS medium- and high-difficulty splits. Single runs, no variance reported; elsewhere the paper lists the full system at 7.53 on VOiCES for what is nominally this same configuration, so treat differences of a tenth of a point as direction, not magnitude.</p>
<p>First, the supervised rows dominate: the corpus alone (unstaged SFT) and the staging contribute roughly equally, and together they account for around four fifths of the total improvement. Second, WER-only RL contributed little: vanilla GRPO lands slightly worse than no RL on VOiCES, and vanilla DAPO leaves VOiCES essentially unchanged while capturing about a quarter of the eventual NOIZEUS gain. Every variant of the redesigned reward moves both benchmarks where WER-only RL moved only one, and the optimizer choice (DAPO over GRPO) is worth less than the reward redesign. Third, among the reward components, removing the sentence-level term costs the most, consistent with the regime boundary. The gating mechanism contributes little: removing the gated fusion costs almost nothing, and the gate-threshold sweep (reported on NOIZEUS only) is nearly flat end to end.</p>
<p>The reward also did not need a model in the loop. Replacing the dynamic reward with a Gemini-2.5-flash-lite judge score changed WER by about 0.1 in both directions across the test sets while roughly tripling the time per RL step. Where the task has a reference transcript, a verifiable rule-based reward already captures what the judge provides.</p>
<h2 id="the-router-degradation-is-cheap-to-detect-so-the-robust-weights-apply-as-a-lora-delta">The router: degradation is cheap to detect, so the robust weights apply as a LoRA delta</h2>
<p>Always-on robust weights cost accuracy on clean input.</p>
<div class="blog-table" role="region" aria-label="Table 2" tabindex="0"><table>
<thead>
<tr>
<th>Benchmark</th>
<th style="text-align:right">Qwen3-ASR</th>
<th style="text-align:right">Mega-ASR</th>
<th style="text-align:right">Mega-ASR routed</th>
</tr>
</thead>
<tbody>
<tr>
<td>LibriSpeech test clean/other</td>
<td style="text-align:right">1.62 / 3.40</td>
<td style="text-align:right">1.78 / 3.57</td>
<td style="text-align:right">1.63 / 3.37</td>
</tr>
<tr>
<td>FLEURS zh / en</td>
<td style="text-align:right">3.93 / 3.19</td>
<td style="text-align:right">5.43 / 3.76</td>
<td style="text-align:right">3.86 / 3.17</td>
</tr>
</tbody>
</table>
</div><p>Mega-ASR paper, Table 3, page 7. WER (%), lower is better. LibriSpeech is read clean English speech; FLEURS is a multilingual read-speech benchmark.</p>
<p>The fix exploits an asymmetry: classifying audio as degraded is far easier than transcribing it. A single-layer Transformer over log-Mel features (a standard spectrogram-style featurization) reaches over 99.5% clean/degraded accuracy. Routing then swaps weights by adding or subtracting precomputed LoRA delta tensors instead of reloading the model, and measured end-to-end runtime on CHiME-4 (a noisy-speech benchmark) changes by -0.8%, within noise. The routed system restores the clean numbers above while keeping most of the robustness.</p>
<p>The asymmetry claim comes with a scope limit: the classifier's accuracy is measured against its own training mixture, clean corpora versus the simulator's output, so it may be detecting the simulator's artifacts rather than degradation in general. The behavioral evidence points the same way. On the NOIZEUS full-benchmark average (a different split from the ablation table), always-on weights stay better than routed, 7.52 against 7.90, and on babble noise (overlapping crowd chatter) at 0 dB the routed system is worse than even the unrouted backbone, a pattern consistent with the router misrouting real degraded audio it never saw in training.</p>
<h2 id="results-gains-concentrate-where-the-baseline-collapses">Results: gains concentrate where the baseline collapses</h2>
<div class="blog-table" role="region" aria-label="Table 3" tabindex="0"><table>
<thead>
<tr>
<th>Model</th>
<th style="text-align:right">CHiME-4 + VOiCES + NOIZEUS average WER</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mega-ASR</td>
<td style="text-align:right">6.70</td>
</tr>
<tr>
<td>Mega-ASR routed</td>
<td style="text-align:right">6.76</td>
</tr>
<tr>
<td>Qwen3-ASR-1.7B</td>
<td style="text-align:right">7.93</td>
</tr>
<tr>
<td>Step-Audio-2-mini</td>
<td style="text-align:right">9.50</td>
</tr>
<tr>
<td>Whisper-Large-v3</td>
<td style="text-align:right">10.72</td>
</tr>
<tr>
<td>Kimi-Audio-7B</td>
<td style="text-align:right">10.74</td>
</tr>
<tr>
<td>Gemini-3-Flash</td>
<td style="text-align:right">15.59</td>
</tr>
</tbody>
</table>
</div><p>Mega-ASR paper, Table 2, page 7. Average WER (%) across the three robustness benchmarks; selected rows of the paper's 13-system table. Qwen3-ASR already led this comparison, so Mega-ASR extends the field's best baseline rather than rescuing a weak one.</p>
<p>The average understates how localized the improvement is. On VOiCES, the far-microphone conditions in the two hardest rooms carry almost all of the gain; the worst subset, room-4 far-microphone babble, falls from 54.01 to 45.69 WER. Close-microphone subsets change little in absolute terms, the easiest NOIZEUS conditions improve mostly in relative terms, and a few moderate CHiME-4 subsets regress. That distribution is what training against the high-WER regime predicts: large recoveries where the baseline loses grounding, small movement where it never did.</p>
<p>On the paper's own Voices-in-the-Wild-Bench, mixed-degradation WER falls from 3.30/5.39 to 2.73/4.57 (real/simulated splits), with Kimi-Audio, the closest external system on that split, about a point and a half behind on both. But 70% of the benchmark comes from the same simulator as the training data, so the simulated columns partly measure distribution fit. An LLM-as-judge evaluation supports the failure-mode story more directly, with the judged hallucination rate falling by about a third and missed content by more than half against the backbone, though the judge protocol is under-specified, so treat those as directional. What remains open is transfer: every result is on Qwen3-ASR-1.7B, and the recipe's value on a backbone with a different acoustic encoder or a stronger language prior is untested.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://arxiv.org/abs/2605.19833">Mega-ASR: Towards In-the-wild² Speech Recognition via Scaling up Real-world Acoustic Simulation (arXiv 2605.19833)</a>: all claims and numbers; the regime boundary and reward design (Sec. 4.2), A2S-SFT staging and preliminary variants (Sec. 4.1, Appendix E.1, Table 24), dataset construction (Sec. 3, Appendix C, Table 17), router (Sec. 4.3, Appendix D), ablation (Table 5), reward-versus-judge comparison (Table 6), clean-cost and robustness tables (Tables 2-3, 13), LLM-judge metrics (Table 7), and Figures 2, 4, 6 reproduced above.</li>
<li><a href="https://huggingface.co/datasets/zhifeixie/Voices-in-the-Wild-2M">Voices-in-the-Wild-2M dataset (Hugging Face)</a>: the released training corpus described in the dataset section (the card lists about 646K examples, a subset of the paper's 2.4M clips).</li>
<li><a href="https://github.com/xzf-thu/Voices-in-the-Wild-Bench">Voices-in-the-Wild-Bench (GitHub)</a>: the released evaluation benchmark cited in the results section.</li>
<li><a href="https://arxiv.org/abs/2601.21337">Qwen3-ASR technical report (arXiv 2601.21337)</a>: the backbone model fine-tuned throughout.</li>
<li><a href="https://arxiv.org/abs/2503.14476">DAPO: An Open-Source LLM Reinforcement Learning System at Scale (arXiv 2503.14476)</a>: the policy-optimization loss used by DG-WGPO.</li>
</ul>
<h2 id="related-diffio-posts">Related Diffio posts</h2>
<ul>
<li><a href="https://diffio.ai/blog/grpo-dapo/">GRPO and DAPO: Within-Group Reward Dispersion Gates the Learning Signal</a>: the group-relative policy optimization mechanism the RL section builds on.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>GRPO and DAPO: Within-Group Reward Dispersion Gates the Learning Signal</title>
      <link>https://diffio.ai/blog/grpo-dapo/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/grpo-dapo/</guid>
      <pubDate>Fri, 12 Jun 2026 21:52:07 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>ML</category>
      <description>GRPO and DAPO, the RL recipes for post-training LLMs against verifiable rewards, and why within-group reward dispersion gates the learning signal.</description>
      <media:content url="https://diffio.ai/media/blog/grpo-dapo/grpo-figure-4.webp" medium="image"/>
      <content:encoded><![CDATA[<p>Group Relative Policy Optimization (GRPO) is the RL algorithm introduced in DeepSeek's DeepSeekMath paper and now the default recipe for post-training LLMs against verifiable rewards. It keeps PPO's clipped policy-gradient surrogate and deletes the learned value-function critic; nothing replaces the critic except the dispersion of rewards across the G responses the policy samples per prompt, so a prompt whose rollouts all score the same contributes no policy gradient at all.</p>
<p>DAPO (Decoupled Clip and Dynamic sAmpling Policy Optimization), from ByteDance Seed and Tsinghua AIR, is the published account of what it takes to run this recipe at scale: four training-detail fixes separate a naive GRPO run on Qwen2.5-32B from a score above DeepSeek's R1 reasoning-RL recipe on competition math.</p>
<h2 id="grpo-replaces-the-learned-critic-with-the-statistics-of-a-rollout-group">GRPO replaces the learned critic with the statistics of a rollout group</h2>
<p>In RLHF-style PPO, the advantage of each generated token comes from generalized advantage estimation over per-token rewards, using a learned value model, with the KL penalty against the reference policy (the frozen model RL starts from) folded into the per-token reward. DeepSeekMath gives two reasons to drop the value model. It is typically another model of comparable size as the policy, a substantial memory and compute burden (argued, not benchmarked: the paper reports no PPO counterpart at 7B). And the reward model scores only completed responses, so a value function that is accurate at every intermediate token must be learned from terminal-only supervision, a credit-assignment problem GRPO avoids setting up at all.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/grpo-dapo/grpo-figure-4.webp" alt="DeepSeekMath Figure 4: PPO and GRPO block diagrams compared" width="1201" height="531" loading="lazy" decoding="async"></figure>

<p>Source: DeepSeekMath paper, Figure 4, p. 13. What to see: PPO (top) routes each output through reward, value, and GAE blocks to get per-token advantages, training two models (yellow); GRPO (bottom) samples G outputs per question, scores them with the reward model (no value model), and a group-computation block turns the G rewards into the G advantages, training only the policy.</p>
<p>GRPO's substitute is the group. For each question, sample G outputs from the current policy, score each with the reward model, and set the advantage of every token of output i to the group-normalized reward:</p>
<pre class="blog-code"><code>A_i = (r_i - mean(r_1..r_G)) / std(r_1..r_G)
</code></pre>
<p>Under this outcome supervision the advantage is constant across an output's tokens, so GRPO is a sequence-level update: each whole response is reinforced or penalized in proportion to how far its reward sits from the group mean, measured in group standard deviations. The paper also argues the group baseline matches how reward models are built, since they are trained on comparisons between outputs to the same question, and a within-question relative advantage asks the reward model only the kind of question it was trained to answer.</p>
<p>Two further changes accompany the group baseline. The KL penalty moves out of the per-token reward and into the loss as an explicit regularizer, estimated with the unbiased, guaranteed-positive estimator from Schulman's KL-approximation note (the k3 form), which keeps the advantage a pure function of the rewards. And the PPO clip survives in the objective but does no work in the original paper's runs: DeepSeekMath takes a single gradient update after each sampling stage, so the policy ratio is 1 at update time and the clip never binds (the paper's own appendix derivations drop the clip under exactly this assumption). The clip binds only in the off-policy regime DAPO runs.</p>
<div class="blog-table" role="region" aria-label="Table 1" tabindex="0"><table>
<thead>
<tr>
<th>Operating point</th>
<th>GRPO (DeepSeekMath)</th>
<th>DAPO</th>
</tr>
</thead>
<tbody>
<tr>
<td>Group size G</td>
<td>64</td>
<td>16</td>
</tr>
<tr>
<td>Batch</td>
<td>1,024 (unit not stated)</td>
<td>512 prompts</td>
</tr>
<tr>
<td>Gradient updates per rollout batch</td>
<td>1</td>
<td>16</td>
</tr>
<tr>
<td>Clip range eps (on the policy ratio)</td>
<td>symmetric, value not reported</td>
<td>0.2 lower / 0.28 upper</td>
</tr>
<tr>
<td>KL term</td>
<td>in loss, k3 estimator, KL coefficient beta 0.04</td>
<td>removed</td>
</tr>
<tr>
<td>Reward</td>
<td>learned reward model</td>
<td>rule-based +1/-1</td>
</tr>
<tr>
<td>Loss reduction</td>
<td>mean per response, then per group</td>
<td>mean over all tokens in the group</td>
</tr>
<tr>
<td>Generation length</td>
<td>max 1,024 tokens</td>
<td>16,384 soft cap, 4,096 penalty band, 20,480 hard cap</td>
</tr>
</tbody>
</table>
</div><p>Source: DeepSeekMath Section 4.2, p. 15; DAPO Sections 2.3, 2.4, 3 and 4.1, pp. 3-8.</p>
<h2 id="uniform-rewards-across-a-group-yield-zero-gradient-from-the-prompt">Uniform rewards across a group yield zero gradient from the prompt</h2>
<p>The advantage formula has an exact consequence: if all G outputs of a question earn the same reward, every advantage is zero (and the standard deviation in the denominator is zero, so the normalization is undefined). The question contributes no policy gradient beyond the KL term. The learning signal exists only on prompts the policy sometimes solves and sometimes fails at the current sampling temperature; prompts the model has mastered and prompts beyond its reach alike contribute nothing. The exact zero is a discrete-reward phenomenon: under a binary verifier, uniform groups are common, while under DeepSeekMath's continuous reward model exact ties are rare and the failure arrives instead as low dispersion, with the std division amplifying whatever reward noise remains. That is consistent with DeepSeekMath never naming the failure mode and specifying no handling for zero-variance groups, and with DAPO, which trains against a binary rule reward, naming it and building a fix into the objective.</p>
<p>A well-calibrated PPO critic produces a soft version of the same zeroing, since a value function that correctly predicts a saturated prompt's reward also sends advantages toward zero. The group baseline zeroes out exactly, by construction, computed from the same batch; because the zero is exact, zero-dispersion groups are countable in every batch, and DAPO counts them: the fraction of prompts whose rollouts all come back correct climbs throughout training and reaches roughly half of each batch late in the run (Figure 3b, p. 5), exactly when the model is strongest. A critic, by contrast, still emits per-token signal on the saturated groups GRPO discards: the critic-based VAPO recipe later reported 60.4 on the same Qwen-32B AIME setup, more than ten points above DAPO, so dropping the value model trades training signal for simplicity, and the comparison is not settled.</p>
<p>The same coupling constrains reward design: a reward whose metric saturates in the regime where the model operates yields no policy gradient even when it is perfectly correct on average. The two classes of fix follow from the two sides of the dependence: resample prompts until every group disperses (DAPO's dynamic sampling, below), or reshape the reward so dispersion survives where the task needs gradient, the route <a href="https://diffio.ai/blog/mega-asr-reward-design/">Mega-ASR's DG-WGPO reward</a> takes for severely degraded speech.</p>
<p>Dividing by the group's standard deviation has a second effect: each prompt's update is measured in units of its own dispersion, so a nearly-uniform group that is not exactly uniform gets amplified advantages. Dr. GRPO later named this a question-level difficulty bias and removed the std term from the advantage entirely.</p>
<h2 id="deepseekmaths-rl-gains-are-reweighting-maj-k-rises-pass-k-does-not">DeepSeekMath's RL gains are reweighting: Maj@K rises, Pass@K does not</h2>
<p>On DeepSeekMath-Instruct 7B, GRPO over roughly 144K math prompts lifts GSM8K from 82.9 to 88.2 and MATH from 46.8 to 51.7 (Table 5, p. 12), with gains on out-of-domain benchmarks that contributed no RL data. The interpretive result is Figure 7: RL raises Maj@K (accuracy of the majority answer over K samples) across K up to 64, but leaves Pass@K (whether any of K samples is correct) essentially unchanged on both benchmarks. The paper's own reading is that the improvement comes from &quot;boosting the correct response from TopK rather than the enhancement of fundamental capabilities&quot;, that is, from reweighting probability mass toward answers the SFT model could already produce. The result is scoped by its setup, which the authors flag themselves: RL prompts came from the instruction-tuning distribution and exploration used naive nucleus sampling. It does not bound RL from a base model: DAPO starts from Qwen2.5-32B scoring near zero on AIME and its pass@32 climbs throughout training alongside avg@32 (DAPO Figure 1, p. 1), with reflection and backtracking behaviors its case study reports as absent early in training.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/grpo-dapo/grpo-figure-7.webp" alt="DeepSeekMath Figure 7: Maj@K and Pass@K for Instruct and RL models on GSM8K and MATH" width="1194" height="580" loading="lazy" decoding="async"></figure>

<p>Source: DeepSeekMath paper, Figure 7, p. 21. What to see: the Maj@K curves separate (orange RL above purple Instruct) on both benchmarks, while the Pass@K curves (blue RL, green Instruct) track each other, with RL slightly below Instruct at large K on both benchmarks.</p>
<p>The paper's Section 5.2 makes the reweighting view systematic by writing SFT, rejection-sampling fine-tuning (RFT), DPO (preference optimization on chosen/rejected response pairs), PPO, and GRPO as one gradient expression that differs only in where the data comes from and what scalar coefficient multiplies each token's log-probability gradient: 1 for SFT, a binary correct/incorrect indicator for RFT, a sigmoid of the preference margin for DPO, the critic's advantage for PPO, the group z-score plus a per-token pull toward the reference model for GRPO. Its 1.3B-scale comparison then isolates two axes: sampling from the current policy beats reusing stale SFT-model samples once the policy has drifted (the RL form of training on self-generated contexts, the distribution-shift mechanism covered in <a href="https://diffio.ai/blog/exposure-bias/">Exposure Bias: Corrupt the Context, Keep the Target Correct</a>), and reward-magnitude-aware coefficients beat binary ones. GRPO sits at the online, graded corner of that grid.</p>
<h2 id="dapo-a-rule-based-reward-no-kl-term-and-four-fixes-between-30-and-50">DAPO: a rule-based reward, no KL term, and four fixes between 30 and 50</h2>
<p>DAPO's starting claim is that R1-style results failed to reproduce not for lack of compute but for lack of training details omitted from the published reports: its naive GRPO baseline on Qwen2.5-32B scores 30 on AIME 2024 (a 30-problem competition-math benchmark scored as avg@32, mean accuracy in percent over 32 samplings of the test set) against DeepSeek's reported 47, with entropy collapse, reward noise, and training instability as the named failure modes. The full system reaches 50 using half the training steps of DeepSeek-R1-Zero-Qwen-32B; all DAPO evidence is this one benchmark on this one base model, without seed variance. The reward is deliberately minimal, +1 for a final answer equivalent to ground truth and -1 otherwise, over a 17K-prompt math set rewritten so every answer is an integer, which makes the rule verifiable without a formula parser.</p>
<p>DAPO also deletes GRPO's KL term entirely. The KL penalty exists in RLHF to keep an aligned model near its initial policy; a long chain-of-thought reasoning model's distribution necessarily diverges far from the base model during training, so the paper removes the penalty rather than penalize an inevitable divergence. The deletion is also cheaper here than in RLHF generally: the KL penalty doubles as protection against over-optimizing a learned reward model, and a rule-based verifier leaves no reward model to over-optimize.</p>
<div class="blog-table" role="region" aria-label="Table 2" tabindex="0"><table>
<thead>
<tr>
<th>Configuration</th>
<th>AIME 2024 avg@32</th>
</tr>
</thead>
<tbody>
<tr>
<td>Naive GRPO</td>
<td>30</td>
</tr>
<tr>
<td>+ Overlong Filtering</td>
<td>36</td>
</tr>
<tr>
<td>+ Clip-Higher</td>
<td>38</td>
</tr>
<tr>
<td>+ Soft Overlong Punishment</td>
<td>41</td>
</tr>
<tr>
<td>+ Token-level Loss</td>
<td>42</td>
</tr>
<tr>
<td>+ Dynamic Sampling (full DAPO)</td>
<td>50</td>
</tr>
<tr>
<td>DeepSeek-R1-Zero-Qwen-32B (reference)</td>
<td>47</td>
</tr>
</tbody>
</table>
</div><p>Source: DAPO Table 1, p. 9. The rows are cumulative in this order, not independent ablations, so each delta is conditional on everything above it; the table supports the mechanism analysis, not a performance ranking.</p>
<h2 id="clip-higher-a-symmetric-clip-zeroes-gradient-on-exactly-the-exploration-tokens">Clip-higher: a symmetric clip zeroes gradient on exactly the exploration tokens</h2>
<p>The first failure is entropy collapse: generation entropy decays toward zero early in training, the G rollouts of each prompt become near-identical, and exploration stops. DAPO's diagnosis is that PPO's symmetric clip is asymmetric in probability space. Clipping zeroes the gradient of tokens whose policy ratio leaves the trust region rather than bounding the ratio itself, and the threshold where gradient stops is multiplicative in the old probability. With eps = 0.2 and a positive advantage, a 0.01-probability token stops receiving gradient once it reaches 0.012, while a 0.9-probability token keeps its gradient up to a nominal 1.08, a bound above probability 1 that can never bind. The upper clip therefore zeroes the gradient of exactly the rare tokens exploration depends on and never touches confident ones. The measurement matches: the mean probability of up-clipped tokens settles below 0.2 after an early transient (Figure 3a, p. 5).</p>
<p>The fix decouples the two clip bounds, raising the upper bound to 0.28 while keeping the lower at 0.2, because raising the lower bound would let negative-advantage updates push token probabilities to zero and collapse the sampling space from the other side. With clip-higher, generation entropy stabilizes around 0.4 instead of decaying to zero, and accuracy climbs past the baseline's plateau.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/grpo-dapo/dapo-figure-2.webp" alt="DAPO Figure 2: AIME accuracy and generation entropy with and without Clip-Higher" width="1301" height="478" loading="lazy" decoding="async"></figure>

<p>Source: DAPO paper, Figure 2, p. 3. What to see: on the right, the baseline's entropy (cyan) decays to near zero by step 1,000 while the clip-higher run (purple) holds around 0.4; on the left, the baseline's accuracy plateaus around 0.25 while the clip-higher run keeps improving.</p>
<p>The clip binds at all only because DAPO takes 16 gradient updates per rollout batch, so by the later minibatches the policy has moved off the rollout distribution and the clip binds throughout training. Clipping, and therefore clip-higher, is the price of reusing rollouts; how hard the clip works is set by how off-policy the update schedule is.</p>
<h2 id="dynamic-sampling-refill-the-batch-until-every-group-has-dispersion">Dynamic sampling: refill the batch until every group has dispersion</h2>
<p>The second failure is the fraction of prompts whose groups have zero reward dispersion and therefore zero advantage. DAPO's fix is a constraint written directly into the objective: the number of correct responses in each group must be strictly between 0 and G. Operationally, over-sample prompts, discard groups that come back all-correct or all-wrong, and keep sampling until the batch is full of prompts with non-degenerate dispersion. Every prompt in every batch then carries gradient. It is the largest single step in Table 1's cumulative ablation, and the dynamic-sampling run reaches the unfiltered baseline's best accuracy in roughly a third of the training steps (Figure 6, p. 8).</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/grpo-dapo/dapo-figure-3.webp" alt="DAPO Figure 3: mean up-clipped probability and the fraction of all-correct groups" width="1300" height="477" loading="lazy" decoding="async"></figure>

<p>Source: DAPO paper, Figure 3, p. 5. What to see: the right panel's all-correct prompt fraction climbing through training, the zero-dispersion share dynamic sampling removes; the left panel's up-clipped tokens sitting at low probabilities, the evidence behind clip-higher.</p>
<p>The filtering is not free: discarded groups cost generation. The paper argues the wall-clock impact is limited because synchronized rollout time is dominated by the longest samples anyway, but Figure 6 counts steps, not GPU-hours, and reports no sampling-overhead measurement, so the efficiency claim is about gradient quality per step, not compute.</p>
<h2 id="token-level-loss-per-response-averaging-shrinks-per-token-gradient-as-responses-grow">Token-level loss: per-response averaging shrinks per-token gradient as responses grow</h2>
<p>GRPO's loss averages per token within each response, then across responses, giving every response equal weight regardless of length. Each token of a long response therefore contributes proportionally less gradient than each token of a short one. In long chain-of-thought training this misweights both directions at once: the reasoning patterns inside high-quality long responses are under-taught, and the gibberish and repetition that low-quality long responses accumulate are under-penalized. DAPO observes the consequence as an unhealthy climb in both entropy and mean response length, with length spiking near 5,000 tokens before swinging erratically (Figure 4, p. 6).</p>
<p>The fix normalizes the summed per-token loss by the total token count across the group, so a generation pattern is promoted or suppressed by the same amount regardless of the length of the response it appears in. In the cumulative ablation it adds a single point, within plausible single-seed noise on a 30-problem benchmark, and the paper credits it with training stability and healthier length growth rather than score. Token-count normalization has its own bias, since a single very long response can dominate its group's update: Dr. GRPO instead removes the per-response length normalization, tracing it to length inflation in incorrect responses, and GSPO later moved the importance ratio and the clip to the sequence level entirely, reporting more stable training, notably for mixture-of-experts models.</p>
<h2 id="overlong-reward-shaping-truncation-penalties-are-reward-noise">Overlong reward shaping: truncation penalties are reward noise</h2>
<p>Generation runs under a hard length cap, and the default treatment punishes truncated samples. That punishment is reward noise: a sound reasoning process gets penalized solely for its length, which, in the paper's words, can confuse the model about the validity of its own reasoning. Simply masking truncated samples out of the loss (Overlong Filtering) is the first row of the cumulative ablation and its second-largest delta, and removing it produces an entropy explosion late in training (Figure 5, p. 7). The shipped version replaces the step penalty with a graded one: reward is untouched up to the soft cap, then decreases linearly to -1 across the penalty band before the hard cap (lengths in the operating-point table). The penalty is then a smooth function of length rather than a truncation event correlated with reasoning depth (the cumulative ablation applies the graded penalty on top of the filtering row; whether the mask is kept in the final recipe the paper does not say).</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://arxiv.org/abs/2402.03300">DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models (arXiv:2402.03300)</a>: GRPO objective, advantage normalization, and critic-removal rationale, Section 4.1, pp. 11-14 (GRPO section, including Figure 4, p. 13, reproduced as the header and first body figure); RL hyperparameters and single-update schedule, Section 4.2, p. 15, with the clip-dropping simplification in Appendix A.1.5-A.1.6 (operating-point table and clip-never-binds claim); GSM8K/MATH results, Table 5, p. 12; Maj@K/Pass@K finding and quote, Section 5.2.2 and Figure 7, p. 21 (reweighting section, including the reproduced figure); unified gradient-coefficient paradigm, Section 5.2.1, Eq. 5 and Table 10, pp. 18-19, and Appendix A.1.</li>
<li><a href="https://arxiv.org/abs/2503.14476">DAPO: An Open-Source LLM Reinforcement Learning System at Scale (arXiv:2503.14476)</a>: naive-GRPO baseline, headline result, and pass@32 curve, Section 1 and Figure 1, pp. 1-2; KL removal, Section 2.3; rule-based reward and dataset, Sections 2.4 and 3.5; clip-higher mechanism and evidence, Section 3.1, Figures 2 and 3a, pp. 3-5 (both reproduced); dynamic sampling, Section 3.2, Figures 3b and 6 (Figure 3 reproduced); token-level loss, Section 3.3, Figure 4; overlong shaping, Section 3.4, Figure 5; hyperparameters, Section 4.1, p. 8; cumulative ablation, Table 1, p. 9; emergent reflection case study, Section 4.4.</li>
<li><a href="http://joschu.net/blog/kl-approx.html">Schulman, 2020, Approximating KL Divergence</a>: the k3 estimator GRPO uses for its loss-side KL term (GRPO section).</li>
<li><a href="https://arxiv.org/abs/2503.20783">Liu et al., 2025, Understanding R1-Zero-Like Training: A Critical Perspective / Dr. GRPO (arXiv:2503.20783)</a>: removal of the std normalization (question-level difficulty bias) and the per-response length normalization (dispersion and token-level loss sections).</li>
<li><a href="https://arxiv.org/abs/2504.05118">Yue et al., 2025, VAPO: Efficient and Reliable Reinforcement Learning for Advanced Reasoning Tasks (arXiv:2504.05118)</a>: value-based 60.4 AIME 2024 result on Qwen-32B under settings matching DAPO (dispersion section's critic trade-off).</li>
<li><a href="https://arxiv.org/abs/2507.18071">Zheng et al., 2025, Group Sequence Policy Optimization (arXiv:2507.18071)</a>: sequence-level importance ratios and clipping, MoE stability claim (token-level loss section).</li>
</ul>
<h2 id="related-diffio-posts">Related Diffio Posts</h2>
<ul>
<li><a href="https://diffio.ai/blog/exposure-bias/">Exposure Bias: Corrupt the Context, Keep the Target Correct</a>: the train/inference context-distribution mismatch behind the online-beats-offline finding in the unified-paradigm comparison.</li>
<li><a href="https://diffio.ai/blog/mega-asr-reward-design/">Mega-ASR: Group-Relative RL Needs a Reward That Ranks Failed Transcripts</a>: the reward-reshaping route in practice, where a saturating WER reward is rebuilt to rank sentence-level failures.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>MOSS-TTS Shows Local RVQ Conditioning Can Beat Delay Modeling</title>
      <link>https://diffio.ai/blog/moss-tts-rvq-depth/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/moss-tts-rvq-depth/</guid>
      <pubDate>Fri, 12 Jun 2026 20:03:14 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>TTS</category>
      <category>ML</category>
      <description>MOSS-TTS, a fully discrete text-to-speech system, shows that conditioning RVQ codec layers locally can beat the delay pattern for predicting multi-codebook audio tokens.</description>
      <media:content url="https://diffio.ai/media/blog/moss-tts-rvq-depth/figure-2.webp" medium="image"/>
      <content:encoded><![CDATA[<p>MOSS-TTS is a fully discrete text-to-speech system: text and prompt audio are serialized into language-model inputs, the model predicts audio codec tokens autoregressively, and a neural codec decoder turns those tokens back into waveform. In the <a href="https://diffio.ai/blog/tts-models-a-map-of-the-modern-speech-stack/">Diffio map of modern TTS systems</a>, that places it in the discrete codec-LM family, where the generated object is not a mel frame or a continuous latent but a matrix of codec tokens.</p>
<p>The MOSS comparison is internal: the report evaluates an 8B delay-pattern generator and a 1.7B Local Transformer under the same tokenizer and pretraining recipe. The comparison is not parameter matched, so it does not isolate causal RVQ depth from every local-module difference. It shows that in this released comparison, the smaller local architecture outperformed the larger delay backbone on the reported cloning metrics.</p>
<h2 id="low-frame-rate-shifts-modeling-cost-to-rvq-depth">Low frame rate shifts modeling cost to RVQ depth</h2>
<p>MOSS-Audio-Tokenizer makes long context possible by using a low frame rate and multiple RVQ layers per frame. It encodes 24 kHz audio at 12.5 frames per second.</p>
<p>Each frame contains 32 residual vector quantization (RVQ) layers. RVQ means the first codebook quantizes a coarse approximation, the next codebook quantizes what the first missed, and later codebooks keep refining the residual.</p>
<p>The report's final pretraining phase measures context in backbone time steps rather than scalar codec decisions. At 12.5 frame steps per second, its 64k-step training window covers about 85 minutes of audio before text, prompt tokens, and the delay model's 31-step edge overhead. The remaining modeling problem is the 32 ordered decisions attached to every frame.</p>
<p>The tokenizer can trade bitrate for residual depth because random quantizer dropout trains the decoder after randomly dropping later RVQ layers, so prefixes of the 32-layer stack remain valid codes. At this frame rate, each additional RVQ layer adds 125 bps.</p>
<div class="blog-table" role="region" aria-label="Table 1" tabindex="0"><table>
<thead>
<tr>
<th style="text-align:right">Bitrate</th>
<th style="text-align:right">RVQ layers used</th>
<th style="text-align:right">English/Chinese SIM</th>
<th style="text-align:right">English/Chinese PESQ-WB</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:right">1000 bps</td>
<td style="text-align:right">8</td>
<td style="text-align:right">0.88 / 0.81</td>
<td style="text-align:right">2.87 / 2.43</td>
</tr>
<tr>
<td style="text-align:right">4000 bps</td>
<td style="text-align:right">32</td>
<td style="text-align:right">0.97 / 0.93</td>
<td style="text-align:right">3.69 / 3.30</td>
</tr>
</tbody>
</table>
</div><p>MOSS-TTS Technical Report Table 2. SIM is cosine-scale speaker-embedding similarity; PESQ-WB is a wideband perceptual speech-quality score. These are tokenizer-oracle reconstruction rows, not a generator-factorization result.</p>
<h2 id="the-delay-pattern-preserves-sequence-length-by-interleaving-rvq-layers">The delay pattern preserves sequence length by interleaving RVQ layers</h2>
<p>The delay pattern keeps the language-model sequence close to the audio frame count. Instead of flattening the RVQ stack into separate audio positions, it shifts each residual layer by its layer index. A <code>T</code>-frame utterance becomes <code>T + 31</code> delayed backbone steps.</p>
<p>Each aligned step has one text/pad channel and one channel per RVQ layer. Channel 0 emits a normal text token on text-only steps and a dedicated pad symbol on audio-frame steps; the audio channels emit RVQ codes. Invalid delayed or padding positions are masked from the loss. In the delay model, one backbone hidden state feeds all channel heads.</p>
<p>This factorization is efficient for serving. The backbone is evaluated once per delayed step, and the output heads map the hidden state to token logits. This is why the report treats the delay architecture as the simpler route for long contexts, duration control, pronunciation control, and optimized inference backends.</p>
<p>The cost is that within-frame RVQ dependencies are represented across multiple delayed steps. Lower layers of a frame are emitted at earlier delayed steps than higher layers. By the time a higher layer is predicted, lower layers can influence it only through intervening input vectors, each built by adding codebook-specific embeddings from whatever delayed layers are present at that step. At any one delayed step, the audio heads are predicting channels from different original frames. The backbone is therefore not just modeling speech over time; it is also tracking incomplete RVQ code stacks across delayed positions.</p>
<h2 id="the-local-transformer-factorizes-each-frame-autoregressively-over-rvq-layers">The Local Transformer factorizes each frame autoregressively over RVQ layers</h2>
<p>The Local Transformer keeps the frame aligned in time and adds a local autoregressive loop over channels. The temporal backbone produces one latent for the frame, then a lightweight local module predicts the text/pad channel and the RVQ layers in order. The text/pad channel keeps that stream in the same factorization as the audio layers. For audio frames it is the pad decision, followed by RVQ layers that condition on earlier layers from the same frame and on previous aligned steps.</p>
<p>That is the inductive bias the MOSS comparison supports for this residual stack. Later RVQ layers are not exchangeable labels; they are refinements of a residual left by earlier layers. Giving layer <code>j</code> the sampled lower-depth codes as direct context makes within-frame RVQ conditioning explicit. The delay pattern can represent that dependency only after the temporal backbone learns to route it through delayed positions.</p>
<p>Moshi is the immediate lineage for this choice: its RQ-Transformer predicts tokens bottom to top inside an audio step, and MOSS adapts that idea to the tokenizer's full RVQ stack. The tradeoff is steady-state cost. The local model runs an inner loop over channels, while the delay model cannot stream-decode a complete frame until the final RVQ layer arrives about 2.5 seconds later.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/moss-tts-rvq-depth/figure-2.webp" alt="MOSS-TTS Figure 2: delay pattern and local transformer architectures" width="1324" height="726" loading="lazy" decoding="async"></figure>

<p>MOSS-TTS Technical Report, Figure 2, p. 8. Delay pattern on the left; frame-local autoregressive loop on the right.</p>
<h2 id="the-smaller-local-model-wins-the-within-moss-cloning-comparison">The smaller local model wins the within-MOSS cloning comparison</h2>
<p>Seed-TTS-eval reports WER for English word error rate, CER for Chinese character error rate, and SIM for speaker-embedding similarity on a percentage scale. This SIM scale differs from the 0-1 reconstruction SIM above.</p>
<div class="blog-table" role="region" aria-label="Table 2" tabindex="0"><table>
<thead>
<tr>
<th>Model</th>
<th>Mode</th>
<th style="text-align:right">Params</th>
<th style="text-align:right">EN WER</th>
<th style="text-align:right">EN SIM</th>
<th style="text-align:right">ZH CER</th>
<th style="text-align:right">ZH SIM</th>
</tr>
</thead>
<tbody>
<tr>
<td>MOSS-TTS</td>
<td>Clone</td>
<td style="text-align:right">8B</td>
<td style="text-align:right">1.92</td>
<td style="text-align:right">69.31</td>
<td style="text-align:right">1.46</td>
<td style="text-align:right">76.21</td>
</tr>
<tr>
<td>MOSS-TTS-Local-Transformer</td>
<td>Clone</td>
<td style="text-align:right">1.7B</td>
<td style="text-align:right">1.87</td>
<td style="text-align:right">71.74</td>
<td style="text-align:right">1.33</td>
<td style="text-align:right">77.24</td>
</tr>
<tr>
<td>MOSS-TTS</td>
<td>Continuation</td>
<td style="text-align:right">8B</td>
<td style="text-align:right">1.84</td>
<td style="text-align:right">70.86</td>
<td style="text-align:right">1.37</td>
<td style="text-align:right">76.98</td>
</tr>
<tr>
<td>MOSS-TTS-Local-Transformer</td>
<td>Continuation</td>
<td style="text-align:right">1.7B</td>
<td style="text-align:right">1.93</td>
<td style="text-align:right">73.28</td>
<td style="text-align:right">1.44</td>
<td style="text-align:right">79.62</td>
</tr>
</tbody>
</table>
</div><p>Seed-TTS-eval, MOSS-TTS Technical Report Table 3. Lower WER/CER (%) is better; higher SIM (%) is better. The report gives no confidence intervals or listening-test variance for this table.</p>
<p>The Local Transformer outperforms the delay model in all four SIM cells, and the highest MOSS similarity value in the table is the local continuation model's Chinese SIM. In Clone mode it also lowers both content-error metrics. In Continuation mode the delay model has slightly lower WER/CER, but both models sit in the low-error regime where the report says manual review found many remaining mismatches to be ASR errors rather than audible pronunciation failures.</p>
<p>CV3-Eval extends the same direction to multilingual cloning, but only for content error. The Local Transformer has lower CER/WER than the delay model in every same-language comparison. Its average absolute improvement over the delay rows is about half a percentage point in both Clone and Continuation mode. That does not establish better speaker preservation outside English and Chinese, because CV3-Eval does not report speaker similarity, but it rules out the narrow explanation that the local model merely trades intelligibility for timbre.</p>
<p>The smaller model outperformed the larger one on the cloning cells the report uses to characterize the local-vs-delay tradeoff, so the evidence supports a scoped design claim: for this tokenizer and release, local conditioning over RVQ depth was the better cloning architecture.</p>
<h2 id="control-and-ultra-long-results-are-delay-only">Control and ultra-long results are delay-only</h2>
<p>The control sections evaluate only the 8B delay model. Duration control, pronunciation control, and ultra-long generation are not reported for the Local Transformer.</p>
<p>The delay architecture supports the control and long-context experiments because it has a simpler single-backbone parameterization. For duration control, the prompt includes a requested audio-token count. The delay model's absolute relative duration error, the generated-duration miss divided by the requested duration, is 0.712% in Chinese and 0.723% in English, using paired pretraining examples with and without explicit token-count prompts rather than a separate duration-control fine-tune.</p>
<p>The ultra-long internal set separates content from speaker drift. In the English <code>50000+</code> input-character bucket, Clone WER reaches 17.49% and Continuation WER reaches 29.52%, while speaker similarity falls to 44.4% for Clone and 51.2% for Continuation.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/moss-tts-rvq-depth/figure-6.webp" alt="MOSS-TTS Figure 6: speaker similarity drift under ultra-long generation" width="1318" height="301" loading="lazy" decoding="async"></figure>

<p>MOSS-TTS Technical Report, Figure 6, p. 21. The English longest-bucket curves separate early and remain low, showing that long-horizon degradation appears as cumulative speaker drift before every sample reaches a lexical failure.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://arxiv.org/abs/2603.18090">MOSS-TTS Technical Report</a>: tokenizer geometry, Figure 2 architecture diagram, delay and Local Transformer objectives, Seed-TTS-eval Table 3, CV3-Eval Table 4, duration Table 5, ultra-long Table 6.</li>
<li><a href="https://arxiv.org/abs/2602.10934">MOSS-Audio-Tokenizer</a>: CAT tokenizer motivation, end-to-end Transformer tokenizer context, and the tokenizer's role as a scalable audio-modeling interface.</li>
<li><a href="https://arxiv.org/abs/2306.05284">MusicGen: Simple and Controllable Music Generation</a>: delay/interleaving-pattern lineage for multi-codebook audio-token modeling.</li>
<li><a href="https://arxiv.org/abs/2410.00037">Moshi: a speech-text foundation model for real-time dialogue</a>: RQ-Transformer lineage and frame-local bottom-to-top token modeling.</li>
</ul>
<h2 id="related-diffio-posts">Related Diffio posts</h2>
<ul>
<li><a href="https://diffio.ai/blog/tts-models-a-map-of-the-modern-speech-stack/">TTS Models: A Map Of The Modern Speech Stack</a></li>
<li><a href="https://diffio.ai/blog/vibevoice-next-token-diffusion/">VibeVoice: Frame Rate Is the Context Budget for Long-Form TTS</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>VibeVoice: Frame Rate Is the Context Budget for Long-Form TTS</title>
      <link>https://diffio.ai/blog/vibevoice-next-token-diffusion/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/vibevoice-next-token-diffusion/</guid>
      <pubDate>Fri, 12 Jun 2026 16:09:38 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>TTS</category>
      <category>ML</category>
      <description>VibeVoice is Microsoft Research&apos;s open-weights system for zero-shot, multi-speaker, long-form speech synthesis: give it a short voice sample per speaker and a script, and a Qwen2.5 backbone generates podcast-style conversation, turn-taking and breaths included, in one streaming left-to-right pass.</description>
      <media:content url="https://diffio.ai/media/blog/vibevoice-next-token-diffusion/figure-1.webp" medium="image"/>
      <content:encoded><![CDATA[<figure class="blog-figure blog-figure--audio"><audio controls preload="metadata" src="https://diffio.ai/media/blog/vibevoice-next-token-diffusion/vibevoice-next-token-diffusion.mp3" aria-label="Listen to article, narrated audio of this post"><a href="https://diffio.ai/media/blog/vibevoice-next-token-diffusion/vibevoice-next-token-diffusion.mp3">Listen to article, narrated audio of this post</a></audio><figcaption>Listen to article</figcaption></figure>

<p>VibeVoice is Microsoft Research's open-weights system for zero-shot, multi-speaker, long-form speech synthesis: give it a short voice sample per speaker and a script, and a Qwen2.5 backbone generates podcast-style conversation, turn-taking and breaths included, in one streaming left-to-right pass. In our <a href="https://diffio.ai/blog/tts-models-a-map-of-the-modern-speech-stack/">map of the modern TTS stack</a> it sits in the continuous-latent autoregressive family. The abstract claims up to 90 minutes of audio with up to 4 speakers; the quantitative evaluation stops at 30 minutes, and the paper never reconciles the two figures. Podcast generation itself is table stakes (NotebookLM, MoonCast, and FireredTTS2 all ship it).</p>
<p>The load-bearing choice is the speech tokenizer's frame rate: 7.5 frames per second, a 3200x downsampling of the 24 kHz waveform, far below the discrete codecs in the paper's comparison tables (40 to 600 tokens per second). That one rate is what fits a 90-minute conversation into a language model's context window. Everything distinctive downstream exists to make the rate survivable: the latent widens to 64 continuous dimensions, a diffusion head replaces the softmax, and semantic content gets its own encoder.</p>
<h2 id="a-7-5-hz-tokenizer-fits-90-minutes-in-context-the-cost-lands-in-channel-width">A 7.5 Hz tokenizer fits 90 minutes in context; the cost lands in channel width</h2>
<p>The acoustic tokenizer emits one 64-dimensional continuous latent per 133 ms of audio, so 90 minutes of speech is 40,500 positions: inside the 65,536-token training context (the final phase of a length curriculum that starts at 4,096) with room left for the script. Long-form generation is then dense attention over one flat sequence, with no chunking, sliding windows, or hierarchical outline model.</p>
<p>What compression removes from the time axis has to reappear in the channel dimension if reconstruction quality is to hold, so the latent must widen as the rate falls. LatentLM, the paper VibeVoice takes its tokenizer recipe from, measures the conversion directly, and the compensation has a ceiling: its deepest compression setting degrades despite a wider latent. VibeVoice's 7.5 Hz / 64-dim operating point is the same one LatentLM ships for speech. SAME makes the same trade for music at 4096x, and our <a href="https://diffio.ai/blog/training-latent-audio-encoders/">latent audio encoder post</a> maps the general design space: rate, width, and latent geometry jointly define the prediction problem the downstream generator faces.</p>
<p>A 64-dim continuous channel is also what retires the softmax. A softmax head picks one codebook entry per step, and no practical codebook covers the information that survives 3200x compression. The discrete route shards each frame across a stack of residual vector quantization (RVQ) codebooks, each quantizing what the previous ones missed: <a href="https://diffio.ai/blog/moss-tts-rvq-depth/">MOSS-TTS</a>, the strongest discrete system at comparable ambition, spends 32 codebooks on every 12.5 Hz frame and pays for them with an inner per-frame decoding loop. VibeVoice samples the whole 64-dim vector jointly through a diffusion head, paying an inner loop of denoising steps instead. A continuous head does not have to be a diffusion head (MELLE regresses mel frames with a variance-augmented head), but denoising from noise samples the full conditional distribution where a regression head commits to a parametric family.</p>
<p>What 7.5 Hz costs in reconstruction:</p>
<div class="blog-table" role="region" aria-label="Table 1" tabindex="0"><table>
<thead>
<tr>
<th>Tokenizer</th>
<th style="text-align:right">Tokens/s</th>
<th style="text-align:right">PESQ</th>
<th style="text-align:right">STOI</th>
<th style="text-align:right">UTMOS</th>
</tr>
</thead>
<tbody>
<tr>
<td>Encodec (8 codebooks)</td>
<td style="text-align:right">600</td>
<td style="text-align:right">2.72</td>
<td style="text-align:right">0.939</td>
<td style="text-align:right">3.04</td>
</tr>
<tr>
<td>DAC (4 codebooks)</td>
<td style="text-align:right">400</td>
<td style="text-align:right">2.74</td>
<td style="text-align:right">0.928</td>
<td style="text-align:right">3.43</td>
</tr>
<tr>
<td>WavTokenizer</td>
<td style="text-align:right">75</td>
<td style="text-align:right">2.37</td>
<td style="text-align:right">0.914</td>
<td style="text-align:right">4.05</td>
</tr>
<tr>
<td>VibeVoice acoustic</td>
<td style="text-align:right">7.5</td>
<td style="text-align:right">3.07</td>
<td style="text-align:right">0.828</td>
<td style="text-align:right">4.18</td>
</tr>
</tbody>
</table>
</div><p>LibriTTS test-clean, from the paper's Table 3. PESQ is an intrusive perceptual speech-quality score with a 4.5 ceiling, STOI a 0–1 intelligibility score computed against the reference, UTMOS a learned 1–5 predictor of human quality ratings.</p>
<p>The continuous tokenizer leads PESQ and UTMOS at one tenth of WavTokenizer's token rate, but token rate is a sequence-length unit, not an information unit: at 16-bit floats the 64-dim latent stream is roughly 7.7 kbps, above every discrete row in the table. The win is positions in the LM's context, not bitrate efficiency.</p>
<p>The weak column is STOI, which the authors attribute to training on podcast audio that deliberately skipped noise reduction. LibriTTS test-clean is clean, so the mismatch runs through the decoder's learned prior: at 3200x the decoder synthesizes fine detail rather than copying it, and detail synthesized under a noisy-podcast prior diverges from the clean reference at exactly the short-time envelope level STOI compares.</p>
<h2 id="the-sigma-vae-tokenizer-sets-latent-variance-for-the-generator-that-consumes-it">The sigma-VAE tokenizer sets latent variance for the generator that consumes it</h2>
<p>The tokenizer is a mirror-symmetric encoder-decoder pair, roughly 340M parameters each, built from seven stages of Transformer blocks whose self-attention is replaced by 1D depth-wise causal convolutions, so encoding and decoding stream; training follows DAC's adversarial recipe. The paper calls it a tokenizer, but it is a VAE variant from LatentLM, the same paper that supplies the generation recipe: the encoder predicts only a mean, and the latent is</p>
<pre class="blog-code"><code>z = mu + sigma * epsilon,   epsilon ~ N(0,1),   sigma ~ N(0, C_sigma)
</code></pre>
<p>with sigma a scalar shared across all 64 channels, drawn fresh per example; VibeVoice sets <code>C_sigma = 0.5</code>.</p>
<p>Taking variance away from the encoder makes the latent space's noise floor a design parameter instead of a training outcome. A vanilla VAE under reconstruction pressure lets per-channel variances collapse toward zero, and once they collapse, every property the generator depends on, the scale of the latents, the spacing between codes, the smoothness of the distribution the diffusion head must learn to sample, is whatever reconstruction happened to leave behind. Fixing sigma pins those properties: information must be carried by mean displacements large enough to clear a noise band of known, channel-uniform width. LatentLM tuned the knob against the consumer rather than the reconstruction: across a sweep of tokenizer variances, generation quality for a next-token-diffusion model tracks the variance while an image-level diffusion model over the same latents is indifferent to it. The band is also the system's tolerance to its own sampling error: every generated latent re-enters the context, and an error smaller than the trained band lands inside the distribution the decoder and the LM already know. <a href="https://diffio.ai/blog/exposure-bias/">Exposure bias</a>, and how variance shaping buys margin against it, gets a full treatment in its own post.</p>
<h2 id="a-diffusion-head-replaces-the-softmax-the-backbone-stays-a-standard-autoregressive-decoder">A diffusion head replaces the softmax; the backbone stays a standard autoregressive decoder</h2>
<p>The prompt sequence concatenates, per speaker, an identifier and that speaker's voice-prompt latents (encoded by the acoustic encoder only; the script never includes the prompt's transcript), followed by speaker-tagged script text and a start-of-speech token. From there the backbone runs the standard autoregressive decoding loop with one substitution. At each step its hidden state conditions a diffusion head, 4 layers and roughly 123M parameters in the 1.5B configuration, in the style MAR introduced for image generation: trained with the standard DDPM noise-prediction L2 objective, it denoises Gaussian noise into the next acoustic latent while the backbone separately predicts whether to emit a termination token. The paper calls each step's output a speech segment without stating whether that is one 7.5 Hz frame or a packed group of frames, the report's largest reproduction gap.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/vibevoice-next-token-diffusion/figure-1.webp" alt="VibeVoice architecture: tokenizers, LLM, diffusion head, and the semantic feedback path" width="1109" height="599" loading="lazy" decoding="async"></figure>

<p>VibeVoice paper, Figure 1, p. 2. Follow the right-hand loop: the diffusion head (D) emits an acoustic latent, the acoustic decoder (A) renders audio, and the semantic encoder (S) re-encodes that audio and adds it back into the next step's input.</p>
<p>At inference the head applies <a href="https://diffio.ai/blog/classifier-free-guidance/">classifier-free guidance</a>, extrapolating away from an unconditional prediction (derived here from the start-of-speech token's hidden state) to sharpen conditioning, at scale 1.3 with 10 denoising steps under DPM-Solver++, a fast ODE solver for diffusion sampling.</p>
<p>The cost structure keeps the language-model serving contract. The backbone runs autoregressively with a KV cache and dominates per-step latency at every profiled setting; the diffusion head's cost scales linearly with its step count, and the feedback path described next adds an acoustic decode and a semantic re-encode inside the loop at around 15 ms each. With everything included, the real-time factor (synthesis time over audio duration; below 1.0 is faster than real time) stays between 0.62 and 0.97 on a single A6000 across both model sizes, generating strictly left-to-right.</p>
<h2 id="semantic-content-comes-from-a-second-frozen-encoder-and-feeds-back-as-input-never-as-a-generation-target">Semantic content comes from a second frozen encoder and feeds back as input, never as a generation target</h2>
<p>The second tokenizer mirrors the acoustic encoder's architecture without the VAE machinery and is trained on an automatic speech recognition (ASR) proxy task: Transformer decoder layers learn to predict transcripts from its output, then the decoder is thrown away and the encoder is frozen. VibeVoice never generates semantic features. Instead, after the model produces a speech segment and decodes it to waveform, the history entry for that step combines two views of the same audio:</p>
<pre class="blog-code"><code>z_p,i = W_a * z_a,i + W_s * SemanticEnc(y_i)
</code></pre>
<p>where <code>z_a</code> is the generated acoustic latent, <code>y</code> is the rendered waveform, and <code>z_p</code> is the hybrid entry that actually enters the LLM's context, built from learned projections of both views.</p>
<p>The semantic entry is a deterministic function of the latent just produced, so it adds no new information to the context. What it adds is a representation: the phonetic content of that latent made explicit, where the acoustic latent buries it in texture optimized for reconstruction. The feedback closes a loop in which the LLM hears the phonetic content of its own output, so when generated audio drifts from the script, the drift is visible in the context rather than something the backbone must learn to extract. The loop is only closed at inference; during training the feedback path reads ground-truth audio, so any corrective use of it is learned implicitly.</p>
<p>That speech generation wants two representations is established lineage: AudioLM generated semantic tokens before acoustic ones, and SpeechTokenizer and Mimi distill semantic content into the first RVQ level of a discrete codec. What VibeVoice's ablation localizes is what goes wrong at a single continuous bottleneck, tested at matched model size and context (1.5B, 16K):</p>
<div class="blog-table" role="region" aria-label="Table 2" tabindex="0"><table>
<thead>
<tr>
<th>History representation</th>
<th style="text-align:right">1-speaker WER-W</th>
<th style="text-align:right">3-speaker WER-W</th>
<th style="text-align:right">Overall WER-W</th>
<th style="text-align:right">Overall SIM-O</th>
</tr>
</thead>
<tbody>
<tr>
<td>Acoustic latents only</td>
<td style="text-align:right">1.06</td>
<td style="text-align:right">13.74</td>
<td style="text-align:right">6.22</td>
<td style="text-align:right">0.68</td>
</tr>
<tr>
<td>One coupled latent (recon + ASR losses)</td>
<td style="text-align:right">6.79</td>
<td style="text-align:right">1.40</td>
<td style="text-align:right">3.55</td>
<td style="text-align:right">0.45</td>
</tr>
<tr>
<td>Hybrid: acoustic + semantic feedback</td>
<td style="text-align:right">1.93</td>
<td style="text-align:right">2.50</td>
<td style="text-align:right">1.84</td>
<td style="text-align:right">0.64</td>
</tr>
</tbody>
</table>
</div><p>Short-set podcast generation (the benchmark's under-12-minute subset), paper's Table 5; the overall columns pool one to four speakers. WER-W is the word error rate of a Whisper transcription of the generated audio against the input script; because the training data was itself filtered by Whisper agreement, the paper cross-checks with a second ASR (WER-N), which preserves the ranking. SIM-O is the cosine similarity between speaker embeddings of the generated audio and the original reference recording, dominated by timbre, the spectral character that makes a voice recognizable.</p>
<p>The acoustic-only row says reconstruction-grade latents preserve timbre (SIM-O matches the best setting) but lose content anchoring exactly when dialogue structure appears: single-speaker error is excellent, and at three speakers it explodes by an order of magnitude.</p>
<p>The coupled row tests the obvious repair, one encoder trained with reconstruction and transcript losses on a single latent, and shows the two objectives competing for fixed capacity. Content stabilizes where dialogue had broken it, but single-speaker error gets worse and the voice goes: overall SIM-O collapses to 0.45, the lowest of all settings in the paper, with the coupled tokenizer's raw reconstruction falling alongside it (PESQ 1.92 against the dedicated acoustic encoder's 2.98 on LibriSpeech test-clean). A 64-dim latent at 3200x has no slack for both objectives, and hard transcript-decoding pressure displaces exactly the texture that makes a voice identifiable. The shipped fix is more capacity, split by objective: a second full encoder, so reconstruction and transcript pressure never compete in one bottleneck.</p>
<p>The dilemma is narrower than the table makes it look. The hybrid pays a visible tax (worse single-speaker error and similarity than acoustic-only), and it binds only because VibeVoice keeps acoustic latents in the LM's context at all: cascades in the CosyVoice style dodge it by generating semantic tokens alone and restoring timbre in a reference-conditioned acoustic decoder, at the price of a second generation stage. SemaVoice serves both objectives with a single latent by softening the pressure, a cosine alignment to frozen WavLM features (a self-supervised speech encoder) at 15 Hz rather than transcript decoding at 7.5 Hz. And the ablation never tests other vehicles for semantic pressure, feeding back ASR text among them, so a separate frozen encoder is the tested fix, not the proven requirement.</p>
<h2 id="enhancement-free-training-data-and-few-denoising-steps-protect-the-same-signal-recording-texture">Enhancement-free training data and few denoising steps protect the same signal: recording texture</h2>
<p>The training corpus is roughly 80B tokens of internal pseudo-labeled podcast audio (the paper does not say how the count splits between text and audio frames), labeled by a pipeline of standard parts: voice-activity segmentation, Whisper transcription, diarization (assigning each segment to a speaker) via WeSpeaker embeddings and HDBSCAN clustering, then filtering by cross-ASR disagreement. The pipeline's one deliberate omission is speech enhancement: denoising removes interjections and prosodic cues along with the noise, so the tokenizer and the LM train on audio with its noise floor, reverb, and mouth sounds intact.</p>
<p>The same trade reappears at inference. The paper's Appendix J frames the diffusion head as a progressive denoiser: more solver steps refine detail but bias the output toward an idealized clean-speech distribution. The step/guidance sweep shows the two metrics pulling in opposite directions: word error is best near 10 steps, while SIM-O is highest at 5 steps and declines monotonically as steps increase.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/vibevoice-next-token-diffusion/figure-3.webp" alt="WER and SIM-O heatmaps across denoising steps and CFG scale" width="1114" height="436" loading="lazy" decoding="async"></figure>

<p>VibeVoice paper, Figure 3, p. 8. The global WER minimum (left) sits at 10 steps, while SIM-O (right) is highest in the 5-step column at every guidance scale and falls as steps grow.</p>
<p>The Appendix J spectrograms show what the extra steps remove: at 50 steps the model strips the prompt's environmental texture (room reverb, noise floor, microphone character), producing audio that is technically cleaner but spectrally mismatched from the reference the speaker embedding is computed on. A speaker's measured identity includes their recording environment, and over-denoising deletes evidence the metric expects to find. How much of that loss is perceptual rather than metric artifact is open: SIM-O is timbre-centric, and in an earlier round of the paper's human evaluation CosyVoice2 outscored VibeVoice-1.5B on SIM-O (0.68 vs 0.55) while human raters judged VibeVoice more similar to the prompt.</p>
<h2 id="results-on-vibevoice-eval-and-a-24-listener-study-the-7b-model-leads-on-an-internal-benchmark">Results on VIBEVOICE-Eval and a 24-listener study: the 7B model leads, on an internal benchmark</h2>
<p>Both evaluations are internal, so the podcast results locate the system rather than certify it: VIBEVOICE-Eval is 108 podcast samples (1 to 30 minutes, built with the same annotation pipeline from sources held out of training), and the listener study covers eight LLM-written two-speaker dialogues averaging 7.3 minutes, rated by 24 annotators.</p>
<div class="blog-table" role="region" aria-label="Table 3" tabindex="0"><table>
<thead>
<tr>
<th>Model</th>
<th style="text-align:right">Subjective avg (1–5)</th>
<th style="text-align:right">WER-W</th>
<th style="text-align:right">SIM-O</th>
</tr>
</thead>
<tbody>
<tr>
<td>ElevenLabs v3 alpha</td>
<td style="text-align:right">3.40</td>
<td style="text-align:right">2.39</td>
<td style="text-align:right">0.623</td>
</tr>
<tr>
<td>Gemini 2.5 Pro preview TTS</td>
<td style="text-align:right">3.66</td>
<td style="text-align:right">1.73</td>
<td style="text-align:right">n/a</td>
</tr>
<tr>
<td>VibeVoice-1.5B</td>
<td style="text-align:right">3.54</td>
<td style="text-align:right">1.11</td>
<td style="text-align:right">0.548</td>
</tr>
<tr>
<td>VibeVoice-7B</td>
<td style="text-align:right">3.76</td>
<td style="text-align:right">1.29</td>
<td style="text-align:right">0.692</td>
</tr>
</tbody>
</table>
</div><p>Paper's Table 1; the subjective average pools realism, richness, and preference scores from the listener study. The paper reports no SIM-O for Gemini.</p>
<p>Scale buys consistency: the 7B leads every subjective dimension and posts the best speaker similarity, while the 1.5B keeps the lowest word error. On the long subset (12 to 30 minutes) the 7B holds WER-W 1.24 with SIM-O 0.75, where MoonCast, limited to two speakers and about ten minutes, crashes on long and 3-plus-speaker cases. Nothing past 30 minutes is measured, and the 7B never trained at the 65,536-token context phase (dropped for resource limits), so the 90-minute figure rests on the 1.5B's final training phase and the context arithmetic. Short-utterance generalization transfers: on the SEED benchmark's Chinese set the 1.5B stays competitive (character error rate 1.16) against systems running 25 to 50 Hz frame rates.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://openreview.net/forum?id=FihSkzyxdv">VibeVoice: Expressive Podcast Generation with Next-Token Diffusion (OpenReview, ICLR 2026)</a>: architecture and training setup (Sections 2–3, Appendix F), reconstruction comparison (Table 3, Table 7), tokenizer ablations (Table 5, Appendices B and D), inference sweep and spectrograms (Figure 3, Appendix J), evaluation (Tables 1–2, Appendices G–I), inference cost (Table 8, Appendix E). Figures 1 and 3 reproduced from this paper. The shorter <a href="https://arxiv.org/abs/2508.19205">arXiv technical report (2508.19205)</a> omits the appendices and ablation tables cited here.</li>
<li><a href="https://github.com/microsoft/VibeVoice">microsoft/VibeVoice on GitHub</a>: released code and checkpoints.</li>
<li><a href="https://arxiv.org/abs/2412.08635">Multimodal Latent Language Modeling with Next-Token Diffusion (LatentLM, arXiv 2412.08635)</a>: sigma-VAE construction (Section 2.3), the tokenizer-variance sweep against DiT (Figure 6), compression-vs-width ablation (Table 6) cited in the frame-rate section.</li>
<li><a href="https://arxiv.org/abs/2605.16964">SemaVoice: Semantic-Aware Continuous Autoregressive Speech Synthesis (arXiv 2605.16964)</a>: the single-latent counterpoint cited in the tokenizer-split section.</li>
</ul>
<h2 id="related-diffio-posts">Related Diffio Posts</h2>
<ul>
<li><a href="https://diffio.ai/blog/exposure-bias/">Exposure bias</a>: why models trained on ground-truth history are brittle to their own samples, and how latent variance shaping buys margin.</li>
<li><a href="https://diffio.ai/blog/classifier-free-guidance/">Classifier-free guidance</a>: the conditioning-sharpening mechanism the diffusion head uses at inference.</li>
<li><a href="https://diffio.ai/blog/training-latent-audio-encoders/">Latent Audio Encoders: Reconstruction Sets the Ceiling, Not the Ranking</a>: the design space of continuous latents built for downstream generators.</li>
<li><a href="https://diffio.ai/blog/moss-tts-rvq-depth/">MOSS-TTS: Modeling The RVQ Stack</a>: the discrete counterpoint, 32 codebooks per frame instead of one wide continuous latent.</li>
<li><a href="https://diffio.ai/blog/tts-models-a-map-of-the-modern-speech-stack/">TTS Models: A Map Of The Modern Speech Stack</a>: where the continuous-latent autoregressive family sits.</li>
<li><a href="https://diffio.ai/blog/same-audio-encoder/">SAME Makes Generation Difficulty an Autoencoder Training Loss</a>: the same rate-for-width trade in music autoencoders.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Exposure Bias: Corrupt the Context, Keep the Target Correct</title>
      <link>https://diffio.ai/blog/exposure-bias/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/exposure-bias/</guid>
      <pubDate>Fri, 12 Jun 2026 16:06:23 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>ML</category>
      <description>Exposure bias is the mismatch between teacher-forced training and free-running inference in autoregressive models. The fix: corrupt the context, keep the target correct.</description>
      <media:content url="https://diffio.ai/media/blog/exposure-bias/figure-6.webp" medium="image"/>
      <content:encoded><![CDATA[<p>Exposure bias is the train/inference mismatch built into almost every autoregressive generative model. Training by teacher forcing evaluates each next-step prediction against a ground-truth history; inference feeds the model its own sampled outputs as history. Every per-step error therefore lands somewhere the training loss never measured, and because the erroneous output becomes the next step's input, the errors compound through the context. Ranzato et al. named the phenomenon in 2016: the model &quot;is only exposed to the training data distribution, instead of its own predictions.&quot;</p>
<p>The mitigation literature looks like a grab bag: scheduled sampling, sequence-level reinforcement learning, interactive imitation learning, target relabeling, and, in modern continuous-latent systems, latent variance shaping and noise injection. One distinction organizes most of it. Sound mitigations do one of two things: they move training onto the context distribution inference actually produces while supplying supervision that is still valid there, or they widen the training distribution of contexts until inference-time errors land inside it. Both respect the same correctness criterion: whatever supervision is attached to a self-generated or corrupted context must still be correct for that context. The criterion is necessary rather than sufficient (how much corruption to budget is a separate question), but the most famous mitigation, scheduled sampling, is the one that violates it, and it is the one with a provably improper objective.</p>
<h2 id="teacher-forcing-fits-every-conditional-under-data-prefixes-inference-queries-model-prefixes">Teacher forcing fits every conditional under data prefixes; inference queries model prefixes</h2>
<p>Autoregressive maximum likelihood factorizes the sequence probability into per-step conditionals and fits each one at ground-truth prefixes. Teacher forcing is not an approximation bolted onto this objective; it is the objective, and it is what makes training cheap: every position becomes an independent supervised problem, a Transformer scores all positions in one forward pass, and the gradient never depends on sampling.</p>
<p>The mismatch appears at decode time. Each conditional was fit under prefixes drawn from the data distribution but is queried under prefixes drawn from the model itself. If the model matched the data exactly, the two prefix distributions would coincide and there would be no bias; exposure bias is entirely a property of model error. That is also why the teacher-forced training and validation losses cannot register it: both are computed under data prefixes, which measure the per-step error seed but not the compounding. Arora et al. later measured this directly, showing that errors accumulate over model-generated context while perplexity fails to capture the accumulation.</p>
<p>Model error makes the mismatch self-amplifying. The first sampling error produces a context slightly off the training distribution; on off-distribution contexts the model's per-step error rate is higher than anything the loss controlled, which pushes the context further off. Bengio et al. put it directly: a mistake made early &quot;can be quickly amplified because the model might be in a part of the state space it has never seen at training time.&quot;</p>
<p>Imitation learning supplies the formal version, because teacher forcing is behavior cloning of the data distribution and exposure bias is its covariate shift. Ross et al. prove that a policy with some per-step error rate under the expert's state distribution can accumulate cost growing quadratically with horizon, while training on the learner's own visited states (with an expert relabeling them) brings the bound back to linear. Quadratic versus linear in sequence length is the worst-case size of the problem.</p>
<h2 id="the-realized-gap-scales-with-model-error-domain-shift-is-where-it-gets-expensive">The realized gap scales with model error: domain shift is where it gets expensive</h2>
<p>Because a perfect model has no exposure bias, the bias concentrates wherever per-step error is highest: long generations, weak models, and inputs far from the training distribution. The worst-case quadratic bound is not the typical case. He et al. compare generation under data prefixes against generation under the model's own prefixes and find the induced distortion limited and non-compounding, crediting a self-recovery ability of language models, so in-domain damage for strong models is often modest.</p>
<p>Domain shift is where the gap gets expensive. Wang and Sennrich's translation models hallucinate (produce fluent output detached from the source) on 1 to 2 percent of in-domain inputs but 33 to 35 percent of out-of-domain inputs, and they attribute part of that gap to exposure bias: minimum risk training, a sequence-level objective computed on the model's own samples, cuts the out-of-domain hallucination rate to 26 to 29 percent without consistently improving in-domain scores. The practical readout: short in-domain benchmarks are nearly blind to exposure bias, which surfaces in long-form generation and off-distribution prompts.</p>
<h2 id="scheduled-sampling-model-contexts-data-targets-improper-objective">Scheduled sampling: model contexts, data targets, improper objective</h2>
<p>Scheduled sampling (Bengio et al., 2015) attacks the prefix mismatch directly. During training, each input token is the ground-truth previous token with some probability and the model's own previous prediction otherwise, with the probability annealed from 1 toward 0 on a linear, exponential, or inverse-sigmoid schedule. The sampling decision is treated as a fixed input; no gradient flows through it. It improved image captioning, parsing, and speech benchmarks, and it was used in the winning entry to the 2015 MSCOCO captioning challenge.</p>
<p>The flaw is that only the context was fixed. The target at each position stays the ground-truth token at that position of the original sequence, regardless of what the sampled prefix now says. Ranzato et al. make the misalignment concrete (their example is aimed at data-as-demonstrator, an earlier method with the same target rule, and applies to scheduled sampling unchanged): the ground truth is &quot;I took a long walk&quot;, the model samples &quot;walk&quot; where the data had &quot;long&quot;, and the next position's target is still &quot;walk&quot;, so the model is taught to emit &quot;walk walk&quot;. The corrupted context changed what the correct continuation is, and the supervision did not follow.</p>
<p>Huszar formalized the damage: the scheduled sampling objective is improper, meaning even infinite data and capacity do not recover the data distribution. In his analysis of length-2 sequences, the fully annealed objective is globally minimized by the product of per-position marginals rather than the joint, and the optimal sequence model &quot;learns to pay no attention whatsoever to the content of the sequence prefix&quot;, using its hidden state as a position counter. The theory matches an empirical detail in the original paper: always sampling from the model, the fully annealed limit, performed very poorly. The method helped anyway because the schedules never reach the degenerate limit in practice: at intermediate mixing rates the procedure widens the prefix distribution around the data (the second mechanism's benefit, obtained without its correctness) while the misaligned-target damage is still diluted.</p>
<h2 id="training-on-self-generated-contexts-rewards-relabeled-targets-matched-dynamics">Training on self-generated contexts: rewards, relabeled targets, matched dynamics</h2>
<p>The repair scheduled sampling needed is supervision that remains correct on model-generated prefixes. Sequence-level training buys it by abandoning per-position targets. MIXER (Ranzato et al., 2016) treats generation as reinforcement learning: the model samples its own continuations, and REINFORCE propagates a sequence-level score (BLEU or ROUGE, n-gram overlap metrics against reference outputs) that is well defined for any sampled sequence. Training prefixes now come from the model by construction, and the objective simultaneously fixes the second mismatch Ranzato identified, training on word-level likelihood while evaluating on sequence-level metrics. The price is a sparse, high-variance learning signal: MIXER only works by warm starting with cross-entropy, keeping the first positions of each sequence teacher-forced while REINFORCE trains the tail, and shrinking the teacher-forced span over training. It improved over the cross-entropy baseline on summarization, translation, and captioning, with greedy decoding beating the baseline's beam search on two of the three tasks.</p>
<p>The move does change what is being optimized: reward maximization rather than distribution matching, so the characteristic failure shifts from compounding drift to metric gaming. Modern RLHF-style post-training inherits both properties: the policy trains on its own rollouts, and its failure mode is reward hacking, not exposure bias.</p>
<p>When the task metric admits computing the best continuation from an arbitrary prefix, per-position targets can be rescued instead of abandoned. Optimal Completion Distillation samples prefixes from the model and sets each target to the tokens that begin edit-distance-optimal suffixes, computable by dynamic programming; this is DAgger's move (roll out the learner, relabel its visited states with an expert) with the dynamic program standing in for the expert, and it reached strong speech recognition results on Wall Street Journal without any likelihood pretraining. Open-ended generation has no such computable expert, which is why rewards became the dominant form there. A third classical family, Professor Forcing, skips supervision pairs entirely and adversarially matches the network's hidden-state dynamics between teacher-forced and free-running modes; it targets the diverging state distributions directly rather than constructing valid training pairs.</p>
<h2 id="continuous-latent-context-turns-occasional-wrong-branches-into-dense-drift">Continuous latent context turns occasional wrong branches into dense drift</h2>
<p>A growing family of generators is autoregressive over continuous vectors rather than discrete tokens: GIVT predicts VAE latents with a Gaussian-mixture head, MAR and LatentLM denoise the next latent with a small diffusion head conditioned on the backbone's hidden state, and TTS systems such as VibeVoice build on the LatentLM recipe (next-token diffusion: the causal Transformer runs once per position, the head iterates only over its own small denoising chain, and the sampled latent feeds back as the next input).</p>
<p>Discreteness was quietly absorbing error all along. Sampling from a softmax snaps the context back onto the lattice of legal token sequences, the way digital signaling regenerates a degraded pulse into a clean symbol: corruption cannot accumulate at the symbol level, only at the distributional level, where legal sequences wander into vanishing-density regions (degenerate repetition loops are exactly such wandering). A sampled continuous vector has no lattice to snap to. It is almost never exactly a point the encoder would have produced, so against a tightly concentrated latent distribution the model is slightly off-distribution at every position, and the design question becomes whether the representation absorbs that error or lets it compound. The contrast is also why this family puts genuine samplers at each step: GIVT's mixture head and MAR's diffusion head both exist, by their authors' statement, because autoregressive generation needs a per-token distribution it can draw samples from. An MSE regression head returns only the conditional mean, which averages modes, and feeding averaged outputs back as context tends to drive generation progressively toward an oversmoothed mean.</p>
<h2 id="variance-shaping-sigma-vae-makes-sampling-error-land-inside-the-trained-band">Variance shaping: sigma-VAE makes sampling error land inside the trained band</h2>
<p>Because the context is now a vector in a space the system designer controls, the second mitigation mechanism becomes available: widen the training distribution itself, an option discrete tokens never had. LatentLM's sigma-VAE is the explicit version. The encoder predicts only a latent mean; the noise scale is removed from its control entirely. A single scalar, drawn afresh for each training example from a zero-mean Gaussian of width C_sigma (a hyperparameter), multiplies unit Gaussian noise added to every channel; the scalar's sign is immaterial since it scales symmetric noise. With the noise scale no longer learnable, the KL term of the ELBO reduces to a penalty on the mean, and training minimizes reconstruction error plus a weighted squared mean norm. A vanilla VAE under reconstruction pressure lets the learned variance of some channels collapse toward zero, leaving a near-deterministic autoencoder whose decoder has only ever seen points on the clean latent manifold. Sigma-VAE makes collapse impossible and the noise floor a knob.</p>
<p>Training latents fill a band of controlled width around each mean, the decoder learns to map the whole band to the correct output, and information is carried by mean displacements large enough to stand clear of the noise. An autoregressive sampling error smaller than the band keeps the context inside the distribution the system was trained on; variance sets the error-tolerance radius of the latent code. Note what does and does not change: the generator itself still trains teacher-forced, and the robustness is bought entirely in the representation underneath it. The corruption also satisfies the correctness criterion by construction, because the noisy latent is a genuine posterior sample, so the reconstruction target is exactly right for it; isotropic noise carries no alternative semantics, and nothing analogous to the scheduled-sampling target misalignment can occur.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/exposure-bias/figure-6.webp" alt="LatentLM Figure 6: FID versus tokenizer variance for DiT and LatentLM at three CFG scales" width="1124" height="466" loading="lazy" decoding="async"></figure>

<p>Source: LatentLM paper, Figure 6, p. 8. What to see: in the left panel (no classifier-free guidance) the orange LatentLM curve improves as tokenizer variance grows while the black DiT curve stays nearly flat; in the center and right panels, guidance bends the LatentLM curves into a U whose high-variance arm rises again. In every panel the star markers, tokenizers tuned for image-level latent diffusion with tiny variance, sit far above the LatentLM curve and roughly on the DiT curve.</p>
<p>LatentLM trains image tokenizers across a sweep of fixed variances and evaluates each latent space under two generators: DiT, a non-autoregressive diffusion Transformer that denoises all positions jointly and so never feeds a sampled latent back as conditioning context for later positions, and LatentLM, which does exactly that at every position. FID (Frechet Inception Distance, an image-generation quality score, lower is better) is nearly insensitive to tokenizer variance for DiT but, without classifier-free guidance (CFG, a sampling-time knob that amplifies conditioning), improves monotonically with variance for LatentLM. The small-variance tokenizers that serve image-level diffusion perfectly are the worst latent spaces for autoregressive consumption. The two generators differ in architecture and objective wholesale, so this is directional evidence rather than a single-variable ablation, but the variance sensitivity tracks exactly the presence of cross-position feedback. The U-shaped curves under guidance are the budget side of the knob: past the optimum, variance spent on robustness is capacity taken from the code. The sweep is image-domain with fixed rather than sampled variances; nothing isolates the knob for speech.</p>
<p>GIVT reports the same trade through a different knob. Raising the VAE's KL weight makes reconstruction worse but autoregressive sampling better, and since reconstruction quality is a ceiling generation cannot exceed, the knob trades ceiling height for how closely the generator approaches it. This is the same decoupling between reconstruction and generation usability documented for non-autoregressive generators in <a href="https://diffio.ai/blog/same-audio-encoder/">SAME Makes Generation Difficulty an Autoencoder Training Loss</a>. How to budget the trade across KL targets, noise injection, normalization, and decoder tolerance is the subject of <a href="https://diffio.ai/blog/training-latent-audio-encoders/">Latent Audio Encoders: Reconstruction Sets the Ceiling, Not the Ranking</a>, which treats latent regularization as deciding which component absorbs prediction error.</p>
<p>VibeVoice is the production endorsement. Its frozen acoustic tokenizer is a sigma-VAE using the sampled-sigma scheme with C_sigma = 0.5, adopted, in the paper's words, to mitigate variance collapse in autoregressive modeling settings, and the system generates up to 90 minutes of multi-speaker audio, the regime where dense latent drift has the longest lever arm. That working value is the only published speech-side variance datapoint we know of in this family; treat it as a starting point, not a tuned optimum.</p>
<h2 id="diffusion-samplers-have-the-same-mismatch-and-the-same-fix-works">Diffusion samplers have the same mismatch, and the same fix works</h2>
<p>The exposure-bias template is not specific to autoregression over positions; it applies wherever a model consumes self-generated state. A diffusion sampler is the clearest second instance: training denoises ground-truth noised inputs, but at inference each denoising step receives the previous step's prediction, a mismatch Ning et al. describe as the exposure bias problem shared with autoregressive methods, with prediction errors accumulating along the chain. Their fix is the sigma-VAE move at the sampler level: perturb the training input with extra Gaussian noise to simulate inference-time prediction error, leaving the target untouched. The perturbation is semantics-free, so the original target remains a valid answer for the perturbed input (the same property that makes the noisy posterior sample a correct training pair above), and the retrained ADM-IP model matches its baseline's quality with far fewer sampling steps (80 versus 1,000 in their headline comparison). Wherever a model consumes self-generated state, this is the property to check first: the supervision attached to a corrupted or self-generated context must still be correct for that context.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://arxiv.org/abs/1511.06732">Ranzato et al., 2016, Sequence Level Training with Recurrent Neural Networks (arXiv:1511.06732)</a>: naming and definition of exposure bias (intro and teacher forcing section); the misaligned-target example against data-as-demonstrator (scheduled sampling section); MIXER mechanism and results (sequence-level training section).</li>
<li><a href="https://arxiv.org/abs/1506.03099">Bengio et al., 2015, Scheduled Sampling for Sequence Prediction with Recurrent Neural Networks (arXiv:1506.03099)</a>: error-amplification framing (teacher forcing section); the sampling mechanism, decay schedules, no-backprop detail, task results, and the always-sampling failure (scheduled sampling section).</li>
<li><a href="https://arxiv.org/abs/1511.05101">Huszar, 2015, How (not) to Train your Generative Model: Scheduled Sampling, Likelihood, Adversary? (arXiv:1511.05101)</a>: improperness of the scheduled sampling objective, the product-of-marginals optimum, and the prefix-ignoring optimal model (scheduled sampling section).</li>
<li><a href="https://arxiv.org/abs/1011.0686">Ross, Gordon, Bagnell, 2011, A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning (arXiv:1011.0686)</a>: quadratic-versus-linear compounding bounds, Theorems 2.1 and 3.2, and the DAgger mechanism (teacher forcing and sequence-level training sections).</li>
<li><a href="https://arxiv.org/abs/2204.01171">Arora et al., 2022, Why Exposure Bias Matters: An Imitation Learning Perspective of Error Accumulation in Language Generation (arXiv:2204.01171)</a>: measured error accumulation and perplexity's blindness to it (teacher forcing section).</li>
<li><a href="https://arxiv.org/abs/1905.10617">He et al., 2021, Exposure Bias versus Self-Recovery: Are Distortions Really Incremental for Autoregressive Text Generation? (arXiv:1905.10617)</a>: the self-recovery finding bounding in-domain damage (model-error section).</li>
<li><a href="https://arxiv.org/abs/2005.03642">Wang and Sennrich, 2020, On Exposure Bias, Hallucination and Domain Shift in Neural Machine Translation (arXiv:2005.03642)</a>: in-domain versus out-of-domain hallucination rates and the minimum risk training result (model-error section).</li>
<li><a href="https://arxiv.org/abs/1810.01398">Sabour, Chan, Norouzi, 2018, Optimal Completion Distillation for Sequence Learning (arXiv:1810.01398)</a>: edit-distance-optimal target relabeling on model-sampled prefixes (sequence-level training section).</li>
<li><a href="https://arxiv.org/abs/1610.09038">Lamb et al., 2016, Professor Forcing: A New Algorithm for Training Recurrent Networks (arXiv:1610.09038)</a>: adversarial matching of teacher-forced and free-running dynamics (sequence-level training section).</li>
<li><a href="https://arxiv.org/abs/2412.08635">Sun et al., 2024, Multimodal Latent Language Modeling with Next-Token Diffusion / LatentLM (arXiv:2412.08635)</a>: next-token diffusion definition (continuous-latent section); sigma-VAE formulation, Section 2.3; the variance sweep, Section 3.1.3 and Figure 6, p. 8 (variance shaping section, including the reproduced figure).</li>
<li><a href="https://arxiv.org/abs/2312.02116">Tschannen, Eastwood, Mentzer, 2023, GIVT: Generative Infinite-Vocabulary Transformers (arXiv:2312.02116)</a>: Gaussian-mixture head (continuous-latent section); the KL-weight versus sampling-quality trade and the reconstruction ceiling (variance shaping section).</li>
<li><a href="https://arxiv.org/abs/2406.11838">Li et al., 2024, Autoregressive Image Generation without Vector Quantization / MAR (arXiv:2406.11838)</a>: diffusion loss as the per-token distribution model for continuous tokens (continuous-latent section).</li>
<li><a href="https://openreview.net/forum?id=FihSkzyxdv">VibeVoice: Expressive Podcast Generation with Next-Token Diffusion (OpenReview, ICLR 2026)</a>: sigma-VAE adoption rationale, Section 2.1; C_sigma = 0.5, Appendix F; 90-minute multi-speaker generation, abstract (variance shaping section). The arXiv technical report (arXiv:2508.19205) omits the appendix carrying the C_sigma value.</li>
<li><a href="https://arxiv.org/abs/2301.11706">Ning et al., 2023, Input Perturbation Reduces Exposure Bias in Diffusion Models (arXiv:2301.11706)</a>: the diffusion-sampler instance and the input-perturbation fix (final section).</li>
</ul>
<h2 id="related-diffio-posts">Related Diffio Posts</h2>
<ul>
<li><a href="https://diffio.ai/blog/training-latent-audio-encoders/">Latent Audio Encoders: Reconstruction Sets the Ceiling, Not the Ranking</a>: the regularization-as-error-absorption framing this post's variance section builds on.</li>
<li><a href="https://diffio.ai/blog/same-audio-encoder/">SAME Makes Generation Difficulty an Autoencoder Training Loss</a>: the reconstruction-versus-generation decoupling in a non-autoregressive setting.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Classifier-Free Guidance: The Guidance Scale Is an Exponent on an Implicit Classifier</title>
      <link>https://diffio.ai/blog/classifier-free-guidance/</link>
      <guid isPermaLink="true">https://diffio.ai/blog/classifier-free-guidance/</guid>
      <pubDate>Fri, 12 Jun 2026 15:56:16 GMT</pubDate>
      <dc:creator>Nathan Harmon</dc:creator>
      <category>ML</category>
      <description>Classifier-free guidance trains one network with condition dropout and extrapolates between its conditional and unconditional predictions. The guidance scale is an exponent on an implicit classifier.</description>
      <media:content url="https://diffio.ai/media/blog/classifier-free-guidance/latentlm-figure-10.webp" medium="image"/>
      <content:encoded><![CDATA[<p>Classifier-free guidance (CFG) trains a conditional generative model with condition dropout, so one network can predict both with and without the conditioning signal. At sampling time the two predictions are combined by extrapolating along their difference, scaled by a guidance weight. Nearly every modern conditional generator exposes that weight: image diffusion models, the per-token diffusion heads that autoregressive TTS systems run on top of an LLM backbone, and, per decoding step, token language models.</p>
<p>The scale is widely described as conditioning strength. That framing predicts the benefit, stronger adherence to the condition, and none of the costs. By Bayes rule, the difference between the conditional and unconditional predictions estimates the gradient of a classifier the generative model already contains, and the guidance weight is an exponent on that classifier's probability. That one fact is what makes the rest predictable: CFG sharpens the sampling distribution rather than improving the model, costs a second forward pass per step, trades diversity for per-sample fidelity, pushes the sampler off its training distribution at high scales, and has no portable &quot;good value&quot;, because the exponent multiplies a quantity whose magnitude is system-specific.</p>
<h2 id="classifier-guidance-needed-a-separate-noise-aware-classifier">Classifier guidance needed a separate noise-aware classifier</h2>
<p>CFG replaced an earlier construction. A diffusion model's denoising network eps(x_t, c) predicts the noise mixed into the current iterate x_t, and up to a known negative scale factor that prediction estimates the score, the gradient of the log density of the noised data. Bayes rule splits the conditional score into two terms:</p>
<pre class="blog-code"><code>grad log p_t(x | c) = grad log p_t(x) + grad log p_t(c | x)
</code></pre>
<p>Dhariwal and Nichol exploited the second term directly: train a separate classifier p(c | x_t) and add its input-gradient, multiplied by a scale s, to the diffusion model's score at every sampling step. The classifier must be trained on noised inputs at every noise level the sampler visits, because a clean-data classifier produces meaningless gradients on the noisy iterates the sampler actually queries.</p>
<p>The scale was already an exponent there. Multiplying the gradient by s turns p(c | x) into p(c | x)^s inside the log, and as a function of x that term up-weights the inputs the classifier assigns to c most confidently, more aggressively as s grows. The exponent was not optional. Guiding an unconditional ImageNet model at s = 1, which is exactly the Bayes decomposition, produced samples that did not visibly match their classes even though the classifier scored them near 50%: the estimated gradient is too weak in practice to carry the conditioning alone. Pushing s from 1 to 10 on ImageNet traded recall (a coverage measure of diversity) from 0.52 down to 0.32 while precision rose from 0.82 to 0.88. The construction worked but carried an operational burden: a separate noise-aware classifier per conditioning type, and for a condition richer than a class label, a free-text prompt, there is no off-the-shelf classifier to train.</p>
<h2 id="bayes-rule-removes-the-classifier-network-not-the-classification">Bayes rule removes the classifier network, not the classification</h2>
<p>Ho and Salimans inverted the same identity. The implicit classifier inside any conditional generative model is p(c | x) proportional to p(x | c) / p(x), so its gradient is the difference between the conditional and unconditional scores. A diffusion model that can produce both predictions does not need the external classifier; the subtraction eps(x_t, c) minus eps(x_t) plays its role.</p>
<p>Both predictions come from one network. During training the condition c is replaced with probability p_uncond by a null token, a learned embedding standing for &quot;no condition&quot;, so the same weights learn p(x | c) and p(x) jointly. In their ablation, dropout rates of 0.1 and 0.2 perform about equally and 0.5 is consistently worse: the unconditional branch needs only a minority of training capacity. Sampling combines the two outputs:</p>
<pre class="blog-code"><code>eps_guided = (1 + w) * eps(x_t, c)  -  w * eps(x_t)
</code></pre>
<p>with guidance strength w, where w = 0 is plain conditional sampling. Most codebases reparameterize with s = 1 + w:</p>
<pre class="blog-code"><code>eps_guided = eps_uncond + s * (eps_cond - eps_uncond)
</code></pre>
<p>so s = 1 means guidance off. The two conventions differ by exactly one, both are in active use, and published &quot;guidance scale&quot; numbers are not comparable without the accompanying equation.</p>
<p>The cost is structural: every sampling step evaluates the full network twice, once per branch, usually run as a doubled batch. When that matters, the guided pair can be distilled into a single student network that matches the combined output directly (Meng et al. 2022), moving the cost from every deployment step to a one-time training job.</p>
<h2 id="the-scale-tilts-the-sampling-distribution-toward-the-implicit-classifiers-modes">The scale tilts the sampling distribution toward the implicit classifier's modes</h2>
<p>Substituting the implicit classifier back into the classifier-guidance formula shows what the sampler is being pointed at, per noise level:</p>
<pre class="blog-code"><code>grad log [ p_t(x | c) * p_implicit(c | x)^w ]
</code></pre>
<p>Each denoising step moves toward a tilted distribution: the model's own conditional, multiplied by the implicit classifier raised to the guidance power. Geometrically, the update extrapolates past the conditional prediction, directly away from the unconditional one: away from what the model expects regardless of the condition, deeper into the region that only the condition explains.</p>
<p>Two gaps separate that picture from exact sampling, and Ho and Salimans state the first themselves: the epsilon outputs are unconstrained network outputs, so their difference is not in general the gradient of any actual classifier. The second is that even with exact scores, steering each noise level toward its tilted marginal does not compose into sampling the tilted data distribution; Bradley and Nakkiran later showed that CFG instead matches a predictor-corrector sampler, alternating a denoising step with a sharpening correction. CFG is a per-step correction that reliably lands in high-confidence regions of the implicit classifier, not exact inference over a known distribution.</p>
<p>The exponent reading predicts the measured trade-offs. Concentrating mass on classifier modes should hurt distribution-level metrics first and per-sample metrics last, and it does: on ImageNet 64x64, FID (a distribution distance that punishes diversity loss and per-sample artifacts alike) is best at the barely-on w = 0.1 and degrades about seventeenfold by w = 4, while Inception Score, which rewards confidently recognizable individual samples, rises monotonically over the same sweep. Ho and Salimans describe the visual version plainly: sample variety decreases as individual sample fidelity increases.</p>
<p>Pushed further, guidance breaks the sampler's own input distribution. The Imagen authors traced high-guidance failure to a train-test mismatch: the guided update implies clean-image estimates outside the [-1, 1] range of training images, the sampler iterates on its own out-of-range outputs, and images come out saturated and unnatural or diverge outright. Their fix, dynamic thresholding, re-clamps the implied clean-image estimate at every step. The portable lesson is the mechanism, not the fix: extrapolation queries the network off its training manifold, and what breaks first is whatever your domain's analog of pixel range is.</p>
<h2 id="the-same-extrapolation-runs-per-token-on-logits-in-autoregressive-models">The same extrapolation runs per token on logits in autoregressive models</h2>
<p>Nothing in the construction is diffusion-specific; it needs only two predictions from one network that differ in conditioning. In discrete autoregressive models the combination runs once per decoding step, on logits. Koel-TTS, an encoder-decoder TTS token model, trains with both the text and the reference speaker audio dropped together at probability 0.1, then decodes with</p>
<pre class="blog-code"><code>logits_guided = logits_uncond + gamma * (logits_cond - logits_uncond)
</code></pre>
<p>before the softmax, where gamma sits in the s-convention (gamma = 1 is guidance off). Here the tilt is exact where diffusion's is heuristic: the softmax renormalizes, so each token is sampled from precisely p_cond^gamma times p_uncond^(1-gamma), normalized. The exactness is per token: the product of per-token tilts is not the tilted distribution over whole sequences, where the same caveat reappears one level up. The unconditional branch consumes the same generated token history with no text or speaker prefix, so its prediction answers &quot;how would this audio continue with no instructions&quot;, and the guided difference amplifies exactly what the instructions explain. Text LLMs use the same per-step recipe to amplify a prompt, contrasting predictions with and without it (Sanchez et al. 2023), and the two branches' different prefixes mean AR CFG doubles the KV cache as well as the per-token compute.</p>
<p>The trade-off lands differently in TTS than in images. Sweeping gamma from 1 to 3, Koel-TTS finds character error rate and speaker-embedding similarity improve together, with the optimum at 2.5: both metrics measure adherence to a conditioning input being amplified, and much of the &quot;diversity&quot; being burned is the unconditioned failure mode, hallucinated words and drifting voice. The cost surfaces where the paper notes it, as reduced variation at higher scales, not in the adherence metrics.</p>
<h2 id="the-unconditional-branch-defines-what-you-extrapolate-away-from">The unconditional branch defines what you extrapolate away from</h2>
<p>What the second branch conditions on is a design choice, and systems diverge on it. Koel-TTS drops text and speaker context together, so guidance amplifies both at once. VibeVoice's report describes no trained null token: its diffusion head's unconditional branch conditions on the hidden state of the start-of-sequence token, a minimally conditioned input the model computes anyway. SemaVoice drops the LLM hidden state but keeps the previously generated acoustic patch in the unconditional branch, so patch-to-patch continuity stays unguided and only the semantic conditioning is sharpened. The negative-prompt field in image generators is this same equation with the null condition replaced by an unwanted description, turning &quot;away from what is typical&quot; into &quot;away from what is specified&quot;.</p>
<p>Two refinements push the lever further. InstructPix2Pix decomposes the condition and runs two differences with two scales, one amplifying agreement with the input image and one with the edit instruction, the practical alternative to dropping all conditions jointly the way Koel-TTS does. Autoguidance (Karras et al. 2024) replaces the unconditional branch with a smaller, less-trained version of the same model, so the extrapolation points away from that weaker model's errors rather than away from the condition's absence, and recovers fidelity without the diversity collapse. In every case the guided sample is defined as much by the negative branch as by the positive one.</p>
<h2 id="tuned-scales-do-not-transfer-across-systems-tasks-or-notation-conventions">Tuned scales do not transfer across systems, tasks, or notation conventions</h2>
<p>Speech systems that all cite the same CFG paper (we locate these architectures in our <a href="https://diffio.ai/blog/tts-models-a-map-of-the-modern-speech-stack/">map of the modern speech stack</a>) ship guidance settings spanning a factor of three, at different denoising step counts:</p>
<div class="blog-table" role="region" aria-label="Table 1" tabindex="0"><table>
<thead>
<tr>
<th>System</th>
<th>CFG site</th>
<th>Published setting</th>
<th>In s-convention (s = 1 is off)</th>
<th>Head denoising steps</th>
</tr>
</thead>
<tbody>
<tr>
<td>VibeVoice</td>
<td>per-token diffusion head</td>
<td>1.3</td>
<td>1.3</td>
<td>10</td>
</tr>
<tr>
<td>Koel-TTS</td>
<td>AR token logits</td>
<td>gamma = 2.5</td>
<td>2.5</td>
<td>none (token model)</td>
</tr>
<tr>
<td>SemaVoice</td>
<td>per-patch diffusion head</td>
<td>w = 2.5</td>
<td>3.5</td>
<td>not stated</td>
</tr>
<tr>
<td>LatentLM (TTS)</td>
<td>per-token diffusion head</td>
<td>4</td>
<td>4</td>
<td>5</td>
</tr>
<tr>
<td>LatentLM (ImageNet)</td>
<td>per-token diffusion head</td>
<td>1.65</td>
<td>1.65</td>
<td>not stated</td>
</tr>
</tbody>
</table>
</div><p>Source: VibeVoice Sec. 3.2 and Fig. 3; Koel-TTS Secs. 2.5 and 3.4; SemaVoice Sec. 3.2.3 (which uses the (1+w) convention, hence the conversion); LatentLM Secs. 3.1 and 3.3.6.</p>
<p>LatentLM is one architecture family, and it needs 1.65 for class-conditional ImageNet but 4 for TTS: neither the architecture nor the codebase pins the value, the task does. And SemaVoice's published 2.5 is 3.5 in the convention the other rows use, because its paper writes the Ho-Salimans (1+w) form; copying a guidance number without its equation silently changes the strength by one full unit.</p>
<p>The spread follows from the exponent reading: the scale multiplies the implicit classifier's log-likelihood ratio, and the magnitude of that ratio depends on how strongly the conditioning pins the output (a transcript plus a speaker prompt constrains speech far harder than a class label constrains an image), on what the unconditional branch conditions on, and on the sampler's step count. The guidance scale is a coefficient on a system-specific signal, not an absolute strength in shared units.</p>
<p>Published sweeps differ in shape, not just in optimum:</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/classifier-free-guidance/latentlm-figure-10.webp" alt="LatentLM ablation of CFG scale and sampling steps for zero-shot TTS" width="1120" height="458" loading="lazy" decoding="async"></figure>

<p>LatentLM paper, Figure 10, p. 14: intelligibility (1 - WER-C, where WER-C is word error rate from a Conformer-Transducer ASR transcription) and speaker similarity over CFG scales 1 to 8 and over diffusion sampling steps. The jump from scale 1 (guidance off) to 2 dwarfs everything after it, and the optimum at 4 sits on a broad plateau.</p>
<figure class="blog-figure blog-figure--image"><img src="https://diffio.ai/media/blog/classifier-free-guidance/vibevoice-figure-3.webp" alt="VibeVoice ablation heatmaps of CFG scale and denoising steps on WER and speaker similarity" width="1114" height="436" loading="lazy" decoding="async"></figure>

<p>VibeVoice paper, Figure 3, p. 8: word error rate and SIM-O (speaker-embedding similarity to the reference audio) over CFG scales 1.25 to 1.35 and denoising steps 5 to 35. Within that narrow scale window, WER still swings from 3.83 to 1.55 depending on the scale-step pair, and the WER optimum (scale 1.25 at 10 steps) disagrees with the similarity optimum (5 steps), so the shipped setting of 1.3 at 10 steps is a compromise.</p>
<p>The operational consequence: treat the guidance scale and step count as one jointly swept pair per system, and when reporting the result, publish the combination equation next to the number. A scale without its equation is not a reproducible setting.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://arxiv.org/abs/2207.12598">Ho &amp; Salimans, Classifier-Free Diffusion Guidance (arXiv 2207.12598)</a>: condition-dropout training and p_uncond ablation (Table 1), the guided combination and w convention (Eq. 6), the implicit-classifier derivation and the not-a-real-gradient caveat, the two-evaluation cost, and the ImageNet 64x64 FID/IS sweep.</li>
<li><a href="https://arxiv.org/abs/2105.05233">Dhariwal &amp; Nichol, Diffusion Models Beat GANs on Image Synthesis (arXiv 2105.05233)</a>: classifier guidance mechanics, noise-aware classifier requirement, gradient scaling as the exponent on p(c|x), the s = 1 failure on an unconditional model, and the precision/recall trade (Table 4).</li>
<li><a href="https://arxiv.org/abs/2205.11487">Saharia et al., Imagen (arXiv 2205.11487)</a>: high-guidance train-test mismatch, out-of-range clean-image estimates, saturation, and dynamic thresholding.</li>
<li><a href="https://arxiv.org/abs/2408.09000">Bradley &amp; Nakkiran, Classifier-Free Guidance is a Predictor-Corrector (arXiv 2408.09000)</a>: CFG does not sample the tilted distribution; supports the exact-sampling caveat.</li>
<li><a href="https://arxiv.org/abs/2502.05236">Koel-TTS (arXiv 2502.05236)</a>: logit-space CFG for an AR TTS model (Sec. 2.5), 10% joint dropout, the gamma sweep and optimum (Sec. 3.4, Fig. 4), and the doubled effective batch.</li>
<li><a href="https://arxiv.org/abs/2412.08635">LatentLM, Multimodal Latent Language Modeling with Next-Token Diffusion (arXiv 2412.08635)</a>: TTS CFG and step ablations (Fig. 10), image-generation guidance settings (Sec. 3.1), WER-C metric definition (Sec. 3.3).</li>
<li><a href="https://openreview.net/forum?id=FihSkzyxdv">VibeVoice (OpenReview, ICLR 2026)</a>: diffusion-head CFG with the start-of-sequence unconditional branch (Eq. 6), shipped settings (Sec. 3.2), and the scale-step heatmaps (Fig. 3). The shorter arXiv technical report (2508.19205) omits the heatmap figure.</li>
<li><a href="https://arxiv.org/abs/2605.16964">SemaVoice (arXiv 2605.16964)</a>: LLM-state dropout with retained previous patch, (1+w) convention at w = 2.5 (Sec. 3.2.3).</li>
<li><a href="https://arxiv.org/abs/2306.17806">Sanchez et al., Stay on Topic with Classifier-Free Guidance (arXiv 2306.17806)</a>: per-step logit CFG in pure text LLMs.</li>
<li><a href="https://arxiv.org/abs/2211.09800">Brooks et al., InstructPix2Pix (arXiv 2211.09800)</a>: two-condition CFG with independent guidance scales (Eq. 3).</li>
<li><a href="https://arxiv.org/abs/2406.02507">Karras et al., Guiding a Diffusion Model with a Bad Version of Itself (arXiv 2406.02507)</a>: autoguidance; replacing the unconditional branch with a degraded model.</li>
<li><a href="https://arxiv.org/abs/2210.03142">Meng et al., On Distillation of Guided Diffusion Models (arXiv 2210.03142)</a>: removing the two-pass cost by distilling the guided pair.</li>
</ul>
<h2 id="related-diffio-posts">Related Diffio Posts</h2>
<ul>
<li><a href="https://diffio.ai/blog/tts-models-a-map-of-the-modern-speech-stack/">TTS Models: A Map of the Modern Speech Stack</a></li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
