<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/rss.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Ben Gubler</title>
        <link>https://www.bengubler.com</link>
        <description>Ben Gubler's personal website. Thoughts on web development, AI, and building things that matter.</description>
        <lastBuildDate>Sat, 07 Mar 2026 21:29:50 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Next.js</generator>
        <language>en</language>
        <copyright>Copyright 2026 Ben Gubler</copyright>
        <item>
            <title><![CDATA[Introducing agentpane]]></title>
            <link>https://www.bengubler.com/posts/2026-03-05-introducing-agentpane?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2026-03-05-introducing-agentpane</guid>
            <pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A local web UI for AI coding agents. Multi-pane, multi-session, streaming — run Claude Code and Codex side by side from your browser.]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="/blog-images/agentpane-setup.png"/><link rel="preload" as="image" href="/blog-images/agentpane-hero.png"/><h1 id="introducing-agentpane"><a aria-hidden="true" tabindex="-1" href="#introducing-agentpane"><span class="icon icon-link"></span></a>Introducing Agentpane</h1>
<p>agentpane is a local web UI for AI coding agents. You run <code>npx agentpane</code>, it opens in your browser, and you can talk to Claude Code, Codex, or any agent that speaks <a href="https://github.com/anthropics/agent-protocol">ACP (Agent Client Protocol)</a> — all from a multi-pane interface with streaming, persistence, and session management:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="npx agentpane
# → http://localhost:6767
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">npx</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> agentpane</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># → http://localhost:6767</span></span></code></pre></figure>
<p>Pick an agent, pick a working directory, and start a session. The agent runs as a subprocess on your machine. No cloud, no deployment, no accounts.</p>
<p><img src="/blog-images/agentpane-setup.png" alt="The agentpane setup screen with agent and directory selection"/></p>
<h2 id="multi-pane-multi-agent"><a aria-hidden="true" tabindex="-1" href="#multi-pane-multi-agent"><span class="icon icon-link"></span></a>Multi-Pane, Multi-Agent</h2>
<p>The main thing I wanted was the ability to run multiple agents at once and see them side by side. agentpane gives you up to four resizable panes with tabs. You can drag sessions from the sidebar into any pane, or drag a tab to split into a new one. Layout persists across refreshes.</p>
<p>This means you can have Claude Code working on your backend in one pane and Codex refactoring your frontend in another — same screen, same app.</p>
<p><img src="/blog-images/agentpane-hero.png" alt="Two sessions running side by side — Codex on the left, Claude Code on the right"/></p>
<h2 id="architecture"><a aria-hidden="true" tabindex="-1" href="#architecture"><span class="icon icon-link"></span></a>Architecture</h2>
<p>The app is two processes:</p>
<ul>
<li><strong>API server</strong> (Hono, port 3456) — spawns agents, manages sessions, handles persistence</li>
<li><strong>Web frontend</strong> (Next.js, port 6767) — the UI, with <code>/api</code> calls rewritten to the backend</li>
</ul>
<p>The backend is built with <a href="https://effect.website/">Effect.ts</a> and uses dependency injection through composable service layers:</p>
<pre __raw_string__="SessionRepo (DB)
  → ConnectionManager (agent subprocess lifecycle)
  → PromptEngine (turn orchestration)
  → EventHub / EventBroadcaster (SSE)
  → WriteQueue (batched DB writes)
"><code>SessionRepo (DB)
  → ConnectionManager (agent subprocess lifecycle)
  → PromptEngine (turn orchestration)
  → EventHub / EventBroadcaster (SSE)
  → WriteQueue (batched DB writes)
</code></pre>
<p>Agents communicate over ACP — JSON-RPC 2.0 over stdio. When you send a prompt, the <code>PromptEngine</code> creates turn records, passes the message to the agent subprocess via ACP, and streams results back through Server-Sent Events.</p>
<h3 id="streaming-with-reconnection-safety"><a aria-hidden="true" tabindex="-1" href="#streaming-with-reconnection-safety"><span class="icon icon-link"></span></a>Streaming with Reconnection Safety</h3>
<p>The frontend subscribes to an SSE endpoint per session. Each event gets a monotonic ID. The <code>EventBroadcaster</code> keeps a ring buffer (512KB / 1000 events) so that if your browser disconnects and reconnects, it replays missed events using the <code>Last-Event-ID</code> header. No gaps in the conversation, no manual refresh needed.</p>
<h3 id="crash-safe-persistence"><a aria-hidden="true" tabindex="-1" href="#crash-safe-persistence"><span class="icon icon-link"></span></a>Crash-Safe Persistence</h3>
<p>All state goes to a local SQLite database at <code>~/.agentpane/agentpane.db</code>. But writes don&#x27;t happen inline — the <code>WriteQueue</code> accumulates operations and flushes them in a batch every 50ms. Before flushing, ops are persisted to a recovery table. If the server crashes mid-flush, it picks up where it left off on restart. No message loss.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="sql" data-theme="github-dark-dimmed github-light" __raw_string__="-- The recovery table
CREATE TABLE write_queue_ops (
  id TEXT PRIMARY KEY,
  session_id TEXT NOT NULL,
  op_json TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT (datetime(&#x27;now&#x27;))
);
"><code data-language="sql" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">-- The recovery table</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">CREATE</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> TABLE</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1"> write_queue_ops</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  id </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">TEXT</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> PRIMARY KEY</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  session_id </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">TEXT</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> NOT NULL</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  op_json </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">TEXT</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> NOT NULL</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  created_at </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">TEXT</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> NOT NULL</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> DEFAULT</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">datetime</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&#x27;now&#x27;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">);</span></span></code></pre></figure>
<h2 id="what-agents-see"><a aria-hidden="true" tabindex="-1" href="#what-agents-see"><span class="icon icon-link"></span></a>What Agents See</h2>
<p>agentpane doesn&#x27;t care what model powers the agent — it talks ACP. When you connect to a session, the <code>ConnectionManager</code>:</p>
<ol>
<li>Resolves the agent binary (walks <code>node_modules/.bin/</code> for npm compatibility)</li>
<li>Spawns it as a child process with stdio pipes</li>
<li>Negotiates ACP capabilities (auth, config, modes, commands)</li>
<li>Attempts session resumption if reconnecting to a previous session</li>
</ol>
<p>The UI adapts to whatever the agent supports. If the agent exposes configuration options, they show up as dropdowns. If it supports slash commands, you get autocomplete. If it streams thought blocks, they render as collapsible sections.</p>
<h2 id="the-frontend"><a aria-hidden="true" tabindex="-1" href="#the-frontend"><span class="icon icon-link"></span></a>The Frontend</h2>
<p>The web app is Next.js 16 with React 19 and the React Compiler — no manual <code>useMemo</code> or <code>useCallback</code> anywhere. State is split into two context layers:</p>
<ul>
<li><strong>SessionProvider</strong> — active session, health checks, session CRUD</li>
<li><strong>LayoutProvider</strong> — pane configuration, tab ordering, drag-and-drop</li>
</ul>
<p>Server state (sessions, conversations, token usage) is managed with TanStack React Query, invalidated on SSE events. Markdown renders as it streams in using <a href="https://github.com/anthropics/streamdown">Streamdown</a> with Shiki syntax highlighting.</p>
<h2 id="try-it"><a aria-hidden="true" tabindex="-1" href="#try-it"><span class="icon icon-link"></span></a>Try It</h2>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="npx agentpane
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">npx</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> agentpane</span></span></code></pre></figure>
<p>The source is on <a href="https://github.com/bgub/agentpane">GitHub</a>. Docs are at <code>apps/docs</code>.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>ml/ai</category>
            <category>open-source</category>
            <category>frontend</category>
        </item>
        <item>
            <title><![CDATA[Introducing helm]]></title>
            <link>https://www.bengubler.com/posts/2026-02-25-introducing-helm?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2026-02-25-introducing-helm</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A typed TypeScript framework for AI agents. Replace dozens of tools with two — search and execute — and sandbox LLM-generated code with granular permissions.]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="/blog-images/helm-demo-app-list-files.png"/><link rel="preload" as="image" href="/blog-images/helm-demo-app.png"/><link rel="preload" as="image" href="/blog-images/helm-demo-app-sandbox-feature.png"/><h1 id="introducing-helm"><a aria-hidden="true" tabindex="-1" href="#introducing-helm"><span class="icon icon-link"></span></a>Introducing Helm</h1>
<p>helm is a typed TypeScript framework for AI agents. Instead of shelling out and parsing strings, agents call typed functions with structured inputs and outputs:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="ts" data-theme="github-dark-dimmed github-light" __raw_string__="import { createHelm, git, fs, grep } from &quot;@bgub/helm&quot;;

const agent = createHelm({
  permissions: {
    &quot;fs.read&quot;: &quot;allow&quot;,
    &quot;fs.write&quot;: &quot;ask&quot;,
    &quot;fs.remove&quot;: &quot;deny&quot;,
    &quot;git.status&quot;: &quot;allow&quot;,
    &quot;git.*&quot;: &quot;ask&quot;,
  },
  onPermissionRequest: async (operation, args) =&gt; {
    return confirm(`Allow ${operation}?`);
  },
})
  .use(fs())
  .use(git())
  .use(grep());

const { staged, unstaged, branch } = await agent.git.status();
const { content } = await agent.fs.read(&quot;./package.json&quot;);
"><code data-language="ts" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> { createHelm, git, fs, grep } </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;@bgub/helm&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> agent</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1"> createHelm</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  permissions: {</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">    &quot;fs.read&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;allow&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">    &quot;fs.write&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;ask&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">    &quot;fs.remove&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;deny&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">    &quot;git.status&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;allow&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">    &quot;git.*&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;ask&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">  onPermissionRequest</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">async</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">operation</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">args</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=&gt;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1"> confirm</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">`Allow ${</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">operation</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">}?`</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">})</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  .</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">use</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">fs</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  .</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">use</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  .</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">use</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">grep</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">staged</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">unstaged</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">branch</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> await</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> agent.git.</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">status</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">content</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> await</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> agent.fs.</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">read</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;./package.json&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>You register &quot;skills&quot; — groups of related operations — with a builder pattern. TypeScript infers the full type at each step. Every operation has a permission level: <code>allow</code> (runs immediately), <code>ask</code> (pauses for approval), or <code>deny</code> (throws <code>PermissionDeniedError</code>). Permissions resolve by precedence: exact match → wildcard → skill author default → global default.</p>
<p>helm ships with built-in skills for the things agents do every day: <code>fs</code>, <code>git</code>, <code>grep</code>, <code>edit</code>, <code>shell</code>, <code>http</code>. You can define custom skills and they get types, search, and permissions for free.</p>
<h2 id="the-demo"><a aria-hidden="true" tabindex="-1" href="#the-demo"><span class="icon icon-link"></span></a>The Demo</h2>
<p>I built a chatbot where the agent has exactly two tools: <code>search</code> and <code>execute</code>.</p>
<p><code>search</code> does keyword lookup over all registered helm operations — the agent calls it to discover what&#x27;s available and learn the function signatures:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="ts" data-theme="github-dark-dimmed github-light" __raw_string__="agent.search(&quot;file read&quot;);
// → [{ qualifiedName: &quot;fs.read&quot;,
//      description: &quot;Read a file and return its content as a string&quot;,
//      signature: &quot;(path: string) =&gt; Promise&lt;{ content: string }&gt;&quot;, ... }]
"><code data-language="ts" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">agent.</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">search</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;file read&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">// → [{ qualifiedName: &quot;fs.read&quot;,</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">//      description: &quot;Read a file and return its content as a string&quot;,</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">//      signature: &quot;(path: string) =&gt; Promise&lt;{ content: string }&gt;&quot;, ... }]</span></span></code></pre></figure>
<p><code>execute</code> takes arbitrary JavaScript code and runs it against the helm agent API. The LLM writes code like:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark-dimmed github-light" __raw_string__="const { staged, unstaged, branch } = await agent.git.status();
return { branch, staged: staged.length, unstaged: unstaged.length };
"><code data-language="js" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">staged</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">unstaged</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">branch</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> await</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> agent.git.</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">status</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> { branch, staged: staged.</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">length</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, unstaged: unstaged.</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">length</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> };</span></span></code></pre></figure>
<p>Two tools in context, regardless of how many skills are registered. The agent discovers what it needs on demand and writes code to use it.</p>
<p><img src="/blog-images/helm-demo-app-list-files.png" alt="The helm demo app listing files in the current directory"/></p>
<h3 id="sandboxing-untrusted-code"><a aria-hidden="true" tabindex="-1" href="#sandboxing-untrusted-code"><span class="icon icon-link"></span></a>Sandboxing Untrusted Code</h3>
<p>The <code>execute</code> tool runs whatever JavaScript the LLM writes. To make that safe, the demo sandboxes it using <a href="https://github.com/endojs/endo/tree/master/packages/ses">SES (Secure ECMAScript)</a> in a child process.</p>
<p>SES <code>lockdown()</code> freezes every JavaScript intrinsic — <code>Object</code>, <code>Array</code>, <code>Promise</code>, <code>Function</code>, all of it. The code runs inside a <code>Compartment</code>, an isolated global scope with access to exactly two things: an <code>agent</code> proxy and a stubbed <code>console</code>. <code>fetch</code>, <code>require</code>, <code>import</code>, <code>process</code>, <code>fs</code> — none of it exists in the compartment. The only way to do anything interesting is through the agent proxy.</p>
<p>The <code>agent</code> inside the sandbox isn&#x27;t the real helm agent — it&#x27;s a recursive <code>Proxy</code> that intercepts property access and function calls. When the code calls <code>agent.git.status()</code>, the proxy sends an IPC message to the parent process. The parent calls the real method on the real helm agent, runs the full permission check, and sends the result back. If the operation is set to <code>&quot;ask&quot;</code>, the parent pauses for user approval before responding. If it&#x27;s <code>&quot;deny&quot;</code>, the error propagates back through IPC.</p>
<p>The sandboxed code has no idea any of this is happening. It just sees its <code>await</code> resolve with a value. The only way to interact with the outside world is through helm&#x27;s permission-gated operations.</p>
<h3 id="the-permission-ui"><a aria-hidden="true" tabindex="-1" href="#the-permission-ui"><span class="icon icon-link"></span></a>The Permission UI</h3>
<p>The chat UI has a sidebar listing every registered skill and operation, each with an allow/ask/deny toggle. Changing a permission takes effect on the next message.</p>
<p><img src="/blog-images/helm-demo-app.png" alt="The tools panel with per-operation permission controls"/></p>
<p>When the LLM hits an operation set to <code>&quot;ask&quot;</code>, the server streams an approval request to the frontend. The tool call shows an inline banner with Allow and Deny buttons. The server blocks on a <code>Promise&lt;boolean&gt;</code> until the user clicks one.</p>
<p><img src="/blog-images/helm-demo-app-sandbox-feature.png" alt="An execute tool call awaiting user approval with Allow and Deny buttons"/></p>
<p>If the user denies, <code>PermissionDeniedError</code> propagates all the way back and the LLM sees it in the tool result. It can explain why it needs the permission, try a different approach, or give up.</p>
<h2 id="inspiration"><a aria-hidden="true" tabindex="-1" href="#inspiration"><span class="icon icon-link"></span></a>Inspiration</h2>
<p>This architecture — giving the agent a code execution tool instead of dozens of individual tools — was inspired by Cloudflare&#x27;s <a href="https://blog.cloudflare.com/code-mode-mcp/">code mode</a> for their MCP server, where they reduced token usage by 99.9% by replacing 2,500+ API endpoint tools with <code>search</code> + <code>execute</code>. <a href="https://x.com/RhysSullivan/status/2019819177473933404">Rhys Sullivan&#x27;s similar idea</a> crystalized the idea for me: the combination of code execution, discoverability, and a granular permission model means the agent can do anything but can&#x27;t go off the rails.</p>
<h2 id="try-it"><a aria-hidden="true" tabindex="-1" href="#try-it"><span class="icon icon-link"></span></a>Try It</h2>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="npm install @bgub/helm
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> @bgub/helm</span></span></code></pre></figure>
<p>The source is on <a href="https://github.com/bgub/helm">GitHub</a>. The demo app is in <code>apps/demo</code>.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>ml/ai</category>
            <category>open-source</category>
            <category>frontend</category>
        </item>
        <item>
            <title><![CDATA[My Internship at Vercel]]></title>
            <link>https://www.bengubler.com/posts/2025-11-20-interning-at-vercel?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2025-11-20-interning-at-vercel</guid>
            <pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[My experience as an intern working on Next.js at Vercel.]]></description>
            <content:encoded><![CDATA[<p>I already wrote about <a href="./2025-09-19-summer-2025-in-review">my personal projects</a> over the last summer, but figured I should finally write about my internship at Vercel! This post has been sitting in my drafts for a while, so I decided to just publish it. Like my last post, this will sound like a mix between a brag list and a personal blog, so apologies ahead of time.</p>
<h2 id="getting-the-internship"><a aria-hidden="true" tabindex="-1" href="#getting-the-internship"><span class="icon icon-link"></span></a>Getting the Internship</h2>
<p>Since I did a study abroad in Morocco in the fall, I was behind on the internship game. I still didn&#x27;t have an offer in late April, so I reached out to Vercel and was luckily able to get one after a few rounds of interviews!</p>
<p>I had initially hoped to join the v0 or AI SDK teams, but I ended up on the Next.js/Turbopack team, which turned out to be great (more about that in a bit). Things were last-minute and hectic, but I moved to SF on June 1st. I lived in a hostel in the Tenderloin until I could find an apartment (I eventually found a $1200 private room in Chinatown — unimaginably expensive for Utah, where I&#x27;m from, but unimaginably cheap compared to other prices in the area.)</p>
<h2 id="what-i-worked-on"><a aria-hidden="true" tabindex="-1" href="#what-i-worked-on"><span class="icon icon-link"></span></a>What I Worked On</h2>
<p>Things were last minute, but I worked with my manager to choose an intern project. We decided that I would work on adding typed routes support to Next.js.</p>
<p>In the end, I worked on TypeScript support broadly in Next.js. I added automatic route type generation, stabilized typed links + type validation, and brought both to Turbopack. I also deprecated the <code>next lint</code> command, brought Biome support to Next, and worked on some internal projects like a traces viewer for Turbopack.</p>
<p>In addition to these, I built a few projects that I couldn&#x27;t bring to completion in time, including a 7-chapter tutorial on building your own React framework (which I mentioned in my summer review and hope to publish soon!)</p>
<h2 id="the-experience"><a aria-hidden="true" tabindex="-1" href="#the-experience"><span class="icon icon-link"></span></a>The Experience</h2>
<p>I really enjoyed Vercel&#x27;s culture. We had an AI-focused hackathon with wide participation — I built a chatbot with code execution capabilities using Sandboxes. Generally people aren&#x27;t super chatty and are fairly &quot;heads-down&quot; grinding on work, but I had some good opportunities to chat with company leadership like Tom Occhino, Malte Ubl, and Guillermo Rauch.</p>
<p>The coolest thing was working with teammates who are insanely talented. I&#x27;ve been doing web dev and using React for over 8 years now, so it felt like meeting your favorite celebrity to chat with and work alongside people who helped build React or other significant parts of the ecosystem!</p>
<p>If you&#x27;re considering working or interning at Vercel, feel free to reach out with questions! I also made some documents with tips that will be shared with new interns.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>random</category>
        </item>
        <item>
            <title><![CDATA[Summer 2025 in Review]]></title>
            <link>https://www.bengubler.com/posts/2025-09-19-summer-2025-in-review?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2025-09-19-summer-2025-in-review</guid>
            <pubDate>Fri, 19 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Things I learned and projects I made.]]></description>
            <content:encoded><![CDATA[<p>Originally I had planned to write about both my internship at Vercel and my personal projects/learning experiences, but I decided to split the topics into two posts. Expect to hear more about my internship soon!</p>
<p>In the meantime, here&#x27;s a brief overview of the non-work-related events of my summer. This will function as a cross between a journal entry, blog post, and &quot;brag list&quot; that potential employers can see ;), so apologies in advance if it seems like I&#x27;m tooting my own horn.</p>
<h2 id="ai-projects"><a aria-hidden="true" tabindex="-1" href="#ai-projects"><span class="icon icon-link"></span></a>AI Projects</h2>
<p>I finally learned how to write GPU kernels using a <a href="https://developer.nvidia.com/blog/even-easier-introduction-cuda/">few</a> <a href="https://codelabs.developers.google.com/your-first-webgpu-app">cool</a> <a href="https://leetgpu.com/">resources</a>! In conjunction with this, I made a (very much WIP) PyTorch-like library for WebGPU-accelerated computation in JavaScript. I&#x27;m calling it <a href="https://github.com/bgub/shade">shade</a> (get it, like <em>shaders</em>?) I have yet to add backpropagation, though.</p>
<p>I also released a few projects related to tokenization, a field which I&#x27;ve been very interested in: <a href="./2025-08-25-tokka-bench-evaluate-tokenizers-multilingual">tokka-bench</a>, a toolkit for evaluating/benchmarking tokenizers across multiple languages, and <a href="http://github.com/bgub/tokka">tokka</a>, a toolkit for easily training tokenizers on custom mixes of data.</p>
<p>Since working with large amounts of data using just HuggingFace Datasets is difficult, I wrote <a href="https://github.com/bgub/hf_to_mds">hf_to_mds</a> — a script for easily converting datasets to MosaicML Streaming format and uploading them to the HuggingFace Hub! I made an <a href="https://huggingface.co/datasets/bgub/wikipedia-mds">MDS version of the Wikipedia dataset</a> and would like to convert more, but am still waiting to hear back about getting my storage limits raised.</p>
<p>Finally, I wrote a <a href="https://www.lesswrong.com/posts/yRoXmjBKJFbc6zSFq/dialects-for-humans-sounding-distinct-from-llms?utm_campaign=post_share&amp;utm_source=link">LessWrong article</a> about how humans are changing their speech patterns to sound distinct from LLMs. It was featured on the frontpage, which is pretty neat!</p>
<h2 id="random-stuff"><a aria-hidden="true" tabindex="-1" href="#random-stuff"><span class="icon icon-link"></span></a>Random Stuff</h2>
<ul>
<li>After years of procrastination, I finally learned the Vim keybindings! It&#x27;s been surprisingly useful. I switched to LazyVim for a while, and now I&#x27;m on Zed.</li>
<li>Speaking of Zed, I&#x27;m working on a LazyVim-inspired config with leader-key keyboard shortcuts. Hope to release it soon! I also merged a PR to <a href="https://github.com/zed-industries/zed/pull/37428">let users hide the title bar</a>.</li>
<li>I finally bit the bullet and rebranded from <code>@nebrelbug</code> to <code>@bgub</code> on GitHub! I&#x27;m <code>@bgub_</code> on X... lmk if you know how I can get <code>@bgub</code>!</li>
<li>This website looks significantly better after I updated the UI with styling help from v0. And I added a cool &quot;Explain Like I&#x27;m 5&quot; button for streaming AI-powered post summaries!</li>
<li>Over the summer I used macOS long-term for the first time. I liked it less than I expected (auto-tiling is much worse than on Linux) so I am still daily-driving Linux on my personal machine. I used Omarchy for a few weeks, then switched back to Fedora with COSMIC desktop, which I <a href="https://www.reddit.com/r/pop_os/comments/1empjaw/5_years_ago_i_suggested_that_system76_write_a/">may have helped inspire</a> (but realistically probably didn&#x27;t.)</li>
<li>Trying out so many different OS options made me tired of setting up my computer over and over, so I learned how to use Nix! I made a config with home-manager for my work MacBook, and an associated <a href="https://github.com/bgub/nix-macos-starter">public template</a>. My setup also works with Linux machines, but I haven&#x27;t upstreamed those changes to the template.</li>
<li>At Vercel, I wrote a 7-chapter tutorial explaining how to build a React framework from scratch. I haven&#x27;t published it yet but hope to soon!</li>
</ul>
<p>Finally, a few updates from the last few weeks (it&#x27;s not technically summer anymore but I&#x27;m too tired to write more posts).</p>
<ul>
<li>I released a <a href="2025-09-17-ts-base-typescript-library-template">template for TS libraries</a> using modern tooling like release-please, Vitest, tsdown, and Biome.</li>
<li>I released a new, ESM-only version of <a href="2025-09-17-introducing-eta-v4">eta</a> using said tooling.</li>
<li>I updated <a href="https://eta.js.org/">eta.js.org</a> and <a href="https://squirrelly.js.org/">squirrelly.js.org</a> to use FumaDocs instead of Docusaurus, support better versioning, etc.</li>
</ul>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>random</category>
        </item>
        <item>
            <title><![CDATA[Introducing ts-base: A Modern TypeScript Library Template]]></title>
            <link>https://www.bengubler.com/posts/2025-09-17-ts-base-typescript-library-template?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2025-09-17-ts-base-typescript-library-template</guid>
            <pubDate>Wed, 17 Sep 2025 18:30:00 GMT</pubDate>
            <description><![CDATA[Build with tsdown, Vitest, release-please, and Biome.]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="/blog-images/ci-run-screenshot.png"/><link rel="preload" as="image" href="/blog-images/release-please-pr.png"/><link rel="preload" as="image" href="/blog-images/npm-trusted-publisher.png"/><p>Eight years ago, I released my first open-source TypeScript library — <a href="https://github.com/squirrellyjs/squirrelly">Squirrelly</a> — which contained two files, <code>package.json</code> and <code>index.js</code>. Five years ago, I released <a href="https://github.com/bgub/eta">Eta</a> with many more features including testing, linting, bundling, and CI/CD.</p>
<p>I thought was a pretty solid development setup, but times change and the JavaScript ecosystem moves fast. New tools have emerged, best practices have evolved, and the complexity of properly publishing an npm package has somehow gotten both easier <em>and</em> more overwhelming at the same time.</p>
<p>Just look at the <code>package.json</code> &quot;exports&quot; field evolution if you want a headache. Or try figuring out the right combination of TypeScript configs, bundlers, and CI workflows to publish a library that works seamlessly across Node, Deno, Bun, and browsers. It&#x27;s surprisingly tricky to get right.</p>
<p>That&#x27;s why I built <a href="https://github.com/bgub/ts-base"><strong>ts-base</strong></a> — a modern TypeScript library starter template that handles all of this complexity for you. It&#x27;s opinionated, battle-tested, and designed to work out-of-the-box with every major JavaScript runtime.</p>
<h2 id="what-is-ts-base"><a aria-hidden="true" tabindex="-1" href="#what-is-ts-base"><span class="icon icon-link"></span></a>What Is TS-Base?</h2>
<p>ts-base is a TypeScript library template that embraces modern tooling and automated workflows. Instead of starting from scratch or copying outdated boilerplate, you get a complete development environment that includes linting, testing, building, releasing, and publishing — all pre-configured and ready to go.</p>
<p>The template is built around three core principles:</p>
<ul>
<li><strong>Multi-runtime first</strong>: Works seamlessly across Node, Deno, Bun, and browsers</li>
<li><strong>Automation over configuration</strong>: Minimal setup, maximum automation</li>
<li><strong>Modern tooling</strong>: ESM-only, latest TypeScript, and carefully chosen dependencies</li>
</ul>
<h2 id="multi-runtime-architecture"><a aria-hidden="true" tabindex="-1" href="#multi-runtime-architecture"><span class="icon icon-link"></span></a>Multi-Runtime Architecture</h2>
<p>The heart of ts-base is its runtime-agnostic design. Instead of trying to make one file work everywhere (and dealing with compatibility headaches), the template uses a clean separation:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark-dimmed github-light" __raw_string__="// src/internal.ts - Core logic, no runtime-specific APIs
export function add(a: number, b: number): number {
  return a + b;
}

export function greet(name: string, options = {}): string {
  const base = `Hello, ${name}`;
  return options.shout ? `${base.toUpperCase()}!` : `${base}.`;
}
"><code data-language="typescript" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">// src/internal.ts - Core logic, no runtime-specific APIs</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1"> add</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">a</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">b</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> a </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> b;</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1"> greet</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">name</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">options</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> {})</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> base</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> `Hello, ${</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">name</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> options.shout </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> `${</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">base</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">toUpperCase</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">()</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">}!`</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> :</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> `${</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">base</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">}.`</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">}</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark-dimmed github-light" __raw_string__="// src/index.ts - Node/Bun adapter
export { add, greet } from &quot;./internal&quot;;
import { randomBytes } from &quot;node:crypto&quot;;

export function getSecureRandomId(): string {
  const timePart = Date.now().toString(36);
  const bytes = randomBytes(12).toString(&quot;base64url&quot;);
  return `${timePart}-${bytes}`;
}
"><code data-language="typescript" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">// src/index.ts - Node/Bun adapter</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> { add, greet } </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;./internal&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> { randomBytes } </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;node:crypto&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1"> getSecureRandomId</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">()</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> timePart</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> Date.</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">().</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">toString</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">36</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> bytes</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1"> randomBytes</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">12</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">).</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">toString</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;base64url&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> `${</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">timePart</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">}-${</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">bytes</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">}</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark-dimmed github-light" __raw_string__="// src/browser.ts - Browser adapter
export { add, greet } from &quot;./internal&quot;;

export function getSecureRandomId(): string {
  const timePart = Date.now().toString(36);
  const array = new Uint8Array(12);
  crypto.getRandomValues(array);
  const rand = btoa(String.fromCharCode(...array))
    .replaceAll(&quot;+&quot;, &quot;-&quot;)
    .replaceAll(&quot;/&quot;, &quot;_&quot;)
    .replaceAll(&quot;=&quot;, &quot;&quot;);
  return `${timePart}-${rand}`;
}
"><code data-language="typescript" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">// src/browser.ts - Browser adapter</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> { add, greet } </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;./internal&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1"> getSecureRandomId</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">()</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> timePart</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> Date.</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">().</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">toString</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">36</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> array</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1"> Uint8Array</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">12</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  crypto.</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">getRandomValues</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(array);</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> rand</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1"> btoa</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(String.</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">fromCharCode</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">array))</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">replaceAll</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;+&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;-&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">replaceAll</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;/&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;_&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">replaceAll</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;=&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> `${</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">timePart</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">}-${</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">rand</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This gives you clean imports for every runtime:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark-dimmed github-light" __raw_string__="// Node/Bun
import { add, getSecureRandomId } from &quot;@your-package/ts-base&quot;;

// Browser (via bundler)
import { add, getSecureRandomId } from &quot;@your-package/ts-base/browser&quot;;

// Deno (direct TypeScript imports)
import {
  add,
  greet,
} from &quot;https://jsr.io/@bgub/ts-base/&lt;version&gt;/src/index.ts&quot;;
"><code data-language="typescript" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">// Node/Bun</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> { add, getSecureRandomId } </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;@your-package/ts-base&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">// Browser (via bundler)</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> { add, getSecureRandomId } </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;@your-package/ts-base/browser&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">// Deno (direct TypeScript imports)</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  add,</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  greet,</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">} </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;https://jsr.io/@bgub/ts-base/&lt;version&gt;/src/index.ts&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>The build system uses <a href="https://tsdown.dev/">tsdown</a> to create two optimized bundles: one for Node environments and a separate minified bundle for browsers, both with sourcemaps.</p>
<h2 id="developer-experience"><a aria-hidden="true" tabindex="-1" href="#developer-experience"><span class="icon icon-link"></span></a>Developer Experience</h2>
<p>ts-base consolidates your tooling around a few excellent choices:</p>
<p><strong>Biome</strong> replaces both ESLint and Prettier with a single, fast tool. No more configuration conflicts or plugin incompatibilities — just consistent formatting and linting that works out of the box.</p>
<p><strong>Vitest</strong> provides lightning-fast testing with built-in coverage reporting and customizable thresholds. Tests run in parallel, support TypeScript natively, and include helpful features like mocking and snapshots.</p>
<p><strong>Size Limit</strong> monitors your bundle size automatically. It runs in CI and comments on pull requests when your changes would increase the bundle size, helping you catch bloat before it ships.</p>
<p>The TypeScript configuration is optimized for modern bundlers with settings like <code>moduleResolution: &quot;bundler&quot;</code> and <code>allowImportingTsExtensions: true</code> that work great with tools like Vite, Rollup, and esbuild.</p>
<h2 id="automated-cicd-pipeline"><a aria-hidden="true" tabindex="-1" href="#automated-cicd-pipeline"><span class="icon icon-link"></span></a>Automated CI/CD Pipeline</h2>
<p>One of ts-base&#x27;s biggest strengths is its complete CI/CD setup. Every aspect of code quality and publishing is automated:</p>
<p><strong>Quality Gates</strong>: Every pull request triggers linting, type checking, testing, and coverage reporting. The CI uploads coverage to Codecov and comments on PRs with size impact reports.</p>
<p><img src="/blog-images/ci-run-screenshot.png" alt="Screenshot of CI/CD run"/></p>
<p><strong>Release Management</strong>: Instead of complex semantic-release configurations, ts-base uses Google&#x27;s Release Please. When commits land on main, Release Please automatically opens a &quot;Release PR&quot; that updates version numbers, generates changelogs, and creates release tags.</p>
<p><strong>Automated Publishing</strong>: When you merge the Release PR, GitHub Actions automatically builds and publishes your package to both npm and JSR with full OIDC provenance and security attestation.</p>
<p><strong>Conventional Commits</strong>: PR titles are automatically linted to follow conventional commit format, ensuring consistent changelog generation.</p>
<h2 id="why-this-approach-works-better"><a aria-hidden="true" tabindex="-1" href="#why-this-approach-works-better"><span class="icon icon-link"></span></a>Why This Approach Works Better</h2>
<p>Most TypeScript library templates I&#x27;ve seen are either too minimal (leaving you to figure out CI, publishing, and multi-runtime support) or overcomplicated with dozens of dependencies. I&#x27;ve seen templates with packages like <code>@commitlint/cli</code>, <code>@commitlint/config-conventional</code>, <code>@semantic-release/changelog</code>, <code>@semantic-release/git</code>, <code>@semantic-release/github</code>, <code>@semantic-release/npm</code>, and more just for CI publishing!</p>
<p>ts-base takes a different approach with just 8 total dev dependencies. By choosing Release Please over semantic-release, Biome over ESLint+Prettier, and Vitest over Jest, you get a simpler dependency graph that&#x27;s easier to maintain and less likely to break.</p>
<p>The automation philosophy means less configuration and fewer places for things to go wrong. Release Please handles version bumping, changelog generation, and release creation in one tool. The GitHub Actions workflows handle everything else.</p>
<h2 id="the-magic-of-release-please"><a aria-hidden="true" tabindex="-1" href="#the-magic-of-release-please"><span class="icon icon-link"></span></a>The Magic of Release Please</h2>
<p><img src="/blog-images/release-please-pr.png" alt="Screenshot of release-please PR"/></p>
<p>Release Please deserves special attention because it transforms how you think about releases. Instead of manually bumping versions or configuring complex semantic-release pipelines, Release Please works like this:</p>
<ol>
<li>You merge commits to <code>main</code> using conventional commit messages</li>
<li>Release Please automatically opens/updates a &quot;Release PR&quot; with version bumps and changelog entries</li>
<li>When you&#x27;re ready to release, simply merge the Release PR</li>
<li>GitHub Actions automatically publishes to npm and JSR</li>
</ol>
<p>The system supports pre-releases too. If you release an alpha or beta version, it automatically publishes under the &quot;next&quot; tag on npm. You can override version bumps using <code>Release-As: 2.0.0</code> in commit messages, and you can maintain multiple release branches (like <code>2.x</code> and <code>3.x</code>) that each get their own Release PRs.</p>
<h2 id="getting-started"><a aria-hidden="true" tabindex="-1" href="#getting-started"><span class="icon icon-link"></span></a>Getting Started</h2>
<p>Setting up ts-base is straightforward:</p>
<ol>
<li>
<p><strong>Clone and customize</strong>: Clone the repository, remove the <code>.git</code> folder, and update <code>package.json</code>, <code>jsr.json</code>, and <code>.release-please-manifest.json</code> with your package details.</p>
</li>
<li>
<p><strong>Claim your package</strong>: Set the version to <code>0.0.0</code> in all config files, then run <code>npm publish</code> locally to claim your package name on npm.</p>
</li>
<li>
<p><strong>Configure publishing</strong>: In npm, set your package to require 2FA for authorization only (not publishing), then add your GitHub workflow as a trusted publisher. On JSR, create your package and add the repository as a trusted source.</p>
</li>
</ol>
<p><img src="/blog-images/npm-trusted-publisher.png" alt="Screenshot of npm publishing settings"/></p>
<ol start="4">
<li>
<p><strong>Set up GitHub</strong>: Push to GitHub, add <code>CODECOV_TOKEN</code> as a repository secret, and configure branch protection rules.</p>
</li>
<li>
<p><strong>Start developing</strong>: Add your code to <code>src/</code>, write tests, and push commits. Release Please will handle the rest.</p>
</li>
</ol>
<p>I recommend configuring GitHub to only allow squash merging and using &quot;pull request title and commit details&quot; as the default commit message. This keeps your commit history clean and ensures conventional commit compliance.</p>
<h2 id="best-practices--tips"><a aria-hidden="true" tabindex="-1" href="#best-practices--tips"><span class="icon icon-link"></span></a>Best Practices &amp; Tips</h2>
<p><strong>Repository Settings</strong>: Enable branch protection on <code>main</code> with required status checks. Disable merge commits to keep history linear.</p>
<p><strong>Entry Points</strong>: Use the main export (<code>@your-package</code>) for Node/Bun, the browser export (<code>@your-package/browser</code>) for bundled browser code, and direct TypeScript imports for Deno.</p>
<p><strong>Customization</strong>: If you don&#x27;t need separate Node/browser builds, delete the unused configuration. The template is designed to be trimmed down to your specific needs.</p>
<p><strong>Testing Strategy</strong>: The template includes examples of testing both shared and platform-specific code, including mocking browser APIs in the Node test environment.</p>
<h2 id="wrapping-up"><a aria-hidden="true" tabindex="-1" href="#wrapping-up"><span class="icon icon-link"></span></a>Wrapping Up</h2>
<p>Publishing a TypeScript library shouldn&#x27;t require a PhD in tooling configuration. ts-base gives you a modern, opinionated foundation that handles the complexity so you can focus on building great software.</p>
<p>The template represents eight years of lessons learned from maintaining open source projects. Ready to try it out? Check out the <a href="https://github.com/bgub/ts-base">ts-base repository</a> and start building your next library.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>frontend</category>
            <category>open-source</category>
        </item>
        <item>
            <title><![CDATA[Introducing Eta v4]]></title>
            <link>https://www.bengubler.com/posts/2025-09-17-introducing-eta-v4?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2025-09-17-introducing-eta-v4</guid>
            <pubDate>Wed, 17 Sep 2025 17:30:00 GMT</pubDate>
            <description><![CDATA[Making Eta ESM-only, changing package scope, and improving CI/CD.]]></description>
            <content:encoded><![CDATA[<p>I released Eta for the first time five years ago! Since then, it&#x27;s grown to be depended on by hundreds of packages and downloaded 1M+ times per week.</p>
<p>I consider Eta to be mostly &quot;a completed work&quot; since v3, which was released in June 2023. But I wasn&#x27;t satisfied with the bundling, testing, linting, and CI/CD aspects of the project so I rewrote it! (Then extracted my changes, after the fact, into a <a href="https://www.bengubler.com/posts/2025-09-17-ts-base-typescript-library-template">new TS library template called ts-base</a>).</p>
<p>There are a few more changes to be aware of:</p>
<ul>
<li>Eta is now ESM-only by default. It&#x27;s 2025 and this is the right choice!</li>
<li>I transferred the repository (and other associated repositories) to my personal GitHub account, <a href="https://github.com/bgub">bgub</a>. Since I&#x27;m the primary contributor, I think this will simplify maintenance and reduce confusion.</li>
<li>Eta will continue to be published on <a href="https://deno.land/x/">https://deno.land/x/</a>, but I recommend that users use <a href="https://jsr.io/@bgub/eta">https://jsr.io/@bgub/eta</a> instead.</li>
<li>To use the browser-compatible version of Eta, you must import from <code>eta/core</code>, since we&#x27;re now using much cleaner <code>&quot;exports&quot;</code> in our <code>package.json</code>.</li>
</ul>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>open-source</category>
        </item>
        <item>
            <title><![CDATA[Introducing tokka-bench]]></title>
            <link>https://www.bengubler.com/posts/2025-08-25-tokka-bench-evaluate-tokenizers-multilingual?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2025-08-25-tokka-bench-evaluate-tokenizers-multilingual</guid>
            <pubDate>Mon, 25 Aug 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[A comprehensive evaluation framework for comparing tokenizers across human and programming languages.]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="/blog-images/tokka-bench-hero.png"/><link rel="preload" as="image" href="https://pbs.twimg.com/media/GGzDbMRasAAZf_D?format=png&amp;name=medium"/><link rel="preload" as="image" href="/blog-images/tokka-bench-efficiency.png"/><link rel="preload" as="image" href="/blog-images/tokka-bench-coverage.png"/><link rel="preload" as="image" href="/blog-images/tokka-bench-word-splitting.png"/><link rel="preload" as="image" href="/blog-images/tokka-bench-subword-fertility.png"/><link rel="preload" as="image" href="/blog-images/tokka-bench-coding-efficiency.png"/><p>(In a hurry? Visit <a href="https://tokka-bench.streamlit.app/">tokka-bench.streamlit.app</a>)</p>
<p><img src="/blog-images/tokka-bench-hero.png" alt="Screenshot of tokka-bench graph"/></p>
<p>Several months ago, I began working on a new project in my free time — pretraining a small, multilingual LLM. As quests tend to do, mine wandered, and I became very interested in one specific aspect of model training: tokenization.</p>
<p>Today I want to share a framework for evaluating tokenizers, but also explain how tokenizers can help us understand:</p>
<ul>
<li>What data sources a given model may have been trained on</li>
<li>Why some LLMs (especially proprietary models, like ChatGPT, Claude, and Gemini) perform vastly better than others at multilingual tasks</li>
<li>Why Claude, Gemini, and GPT 4o onwards have closed-source tokenizers</li>
<li>Why some OSS models are better than others for fine-tuning</li>
</ul>
<h2 id="technical-background"><a aria-hidden="true" tabindex="-1" href="#technical-background"><span class="icon icon-link"></span></a>Technical Background</h2>
<h3 id="script-encoding--grammar"><a aria-hidden="true" tabindex="-1" href="#script-encoding--grammar"><span class="icon icon-link"></span></a>Script Encoding &amp; Grammar</h3>
<p>Understanding tokenization starts with understanding how text is encoded at the byte level. All language is encoded with UTF-8, but different scripts require vastly different numbers of bytes to encode the same semantic content. English averages just above 1 byte per character, making it incredibly compact. Arabic needs 2+ bytes per character, while Chinese can require 3+ bytes per character to properly encode.</p>
<p>Beyond encoding efficiency, languages have fundamental grammatical differences that affect how information is packed into words. Synthetic languages will pack a lot of syntactic information into single words. For example, I speak Czech, where a phrase like &quot;vzali se&quot; would translate to &quot;they married each other&quot; in English. This grammatical density makes it difficult to compare encoding efficiency.</p>
<h3 id="tokenization"><a aria-hidden="true" tabindex="-1" href="#tokenization"><span class="icon icon-link"></span></a>Tokenization</h3>
<p>LLMs don&#x27;t operate directly on bytes — they operate on &quot;tokens&quot;, which are like symbols corresponding to groups of bytes. Most modern tokenizers use Byte Pair Encoding (BPE), starting with individual bytes and iteratively merging the most frequent pairs to build up a vocabulary of subword units.</p>
<p>There are some alternative approaches like the <a href="https://ai.meta.com/research/publications/byte-latent-transformer-patches-scale-better-than-tokens/">Byte Latent Transformer</a>, but so far they haven&#x27;t really taken off yet in production systems.</p>
<p>The technical decisions in tokenizer design are numerous and consequential:</p>
<ul>
<li>Do you add prefix spaces? (So the &quot;hello&quot; in &quot;hello world&quot; and &quot; hello world&quot; are tokenized the same?)</li>
<li>Do you disallow byte merges across whitespace boundaries? What about across script boundaries?</li>
<li>Do you use an unknown (UNK) token or fallback to bytes when encountering out-of-vocabulary sequences?</li>
</ul>
<p>Hopefully this helps you understand Karpathy’s classic <a href="https://x.com/karpathy/status/1759996551378940395">post on tokenization</a>:</p>
<p><img src="https://pbs.twimg.com/media/GGzDbMRasAAZf_D?format=png&amp;name=medium" alt="Quote by Karpathy: &quot;Tokenization is at the heart of much weirdness of LLMS...&quot;"/></p>
<h2 id="how-tokenization-affects-pretraining"><a aria-hidden="true" tabindex="-1" href="#how-tokenization-affects-pretraining"><span class="icon icon-link"></span></a>How Tokenization Affects PretrAIning</h2>
<p>The relationship between tokenizers and pretraining data creates a complex web of effects that fundamentally shape model capabilities. Tokenizers are often trained on the pretraining data of the LLM they will be used in, but different languages get different levels of &quot;coverage&quot; in the tokenizer vocabulary.</p>
<p>Let’s take an example: Khmer. Since Khmer has fewer online resources, less of a tokenizer’s vocabulary will represent decodings into Khmer than English. This coverage disparity means that encoding the same number of words in Khmer will require many more tokens than English. But here&#x27;s where it gets problematic: pretraining often uses proportional splits of different languages based on token count. This means that you might train on 10 million tokens of English text and 1 million tokens of Khmer, hoping to have a 10:1 ratio of content. But the Khmer text actually represents way less than 10% of the words compared to the English text!</p>
<p>The semantic implications are even more severe. Khmer tokens, because there are fewer, are more likely to represent letters or consonant pairs rather than whole semantic units. This means that models can&#x27;t &quot;store&quot; concepts, attributes, definitions, and other semantic knowledge in embedding vectors quite as easily for underrepresented languages.</p>
<p>There&#x27;s a vibrant open-source community making fine-tunes of OSS foundation models for smaller languages. If your tokenizer doesn&#x27;t handle foreign languages well, fine-tuning will be more difficult and probably require extending the tokenizer with custom tokens. On the other hand, introducing &quot;partially-trained&quot; tokens (tokens that won&#x27;t show up in the pretraining data) can confuse the LLM and even allow for &quot;<a href="https://x.com/karpathy/status/1789590397749957117">token attacks</a>.&quot;</p>
<h2 id="how-tokenization-affects-inference"><a aria-hidden="true" tabindex="-1" href="#how-tokenization-affects-inference"><span class="icon icon-link"></span></a>How Tokenization Affects Inference</h2>
<p>The tokenization disparities that emerge during pretraining continue to create problems during inference. Text in low-resource languages (languages with few online resources) takes many more tokens to represent, causing multiple cascading issues:</p>
<p><strong>Performance degradation</strong>: Slower throughput becomes a significant issue when every sentence requires 2-3x more tokens to represent. Users get sluggish responses, and serving chats costs providers more money.</p>
<p><strong>Context limitations</strong>: Longer sequences fill up the context window faster, and recall performance degrades as the model struggles to maintain coherent understanding across the inflated token sequences.</p>
<p><strong>Generation quality</strong>: Token selection during generation can introduce errors. More tokens per word means more &quot;chances to mess up&quot; per word, potentially leading to compounding drift where small errors in token selection cascade into larger semantic failures.</p>
<h2 id="evaluating-tokenizers-with-tokka-bench"><a aria-hidden="true" tabindex="-1" href="#evaluating-tokenizers-with-tokka-bench"><span class="icon icon-link"></span></a>Evaluating Tokenizers with Tokka-Bench</h2>
<p>I built a tool to easily explore tokenizer performance across 100 natural languages and 20 programming languages. I started by evaluating 7 tokenizers: Gemma 3, GPT-2, GPT-4, gpt-oss, Kimi K2, Llama 3, and Qwen 3.</p>
<p>The project has multiple components designed for different use cases:</p>
<p><strong>Open-source repository</strong>: You can clone it and run benchmarks locally. <a href="https://github.com/bgub/tokka-bench">https://github.com/bgub/tokka-bench</a></p>
<p><strong>Live dashboard</strong>: In addition to the code for running the benchmarks, I also made a live dashboard! <a href="https://tokka-bench.streamlit.app/">https://tokka-bench.streamlit.app/</a></p>
<p>This allows you to easily select combinations of languages and tokenizers to compare, and switch between different metrics to understand the multifaceted nature of tokenizer performance.</p>
<h3 id="datasets-and-methodology"><a aria-hidden="true" tabindex="-1" href="#datasets-and-methodology"><span class="icon icon-link"></span></a>Datasets and Methodology</h3>
<p><strong>Datasets</strong>: For evaluation, I use three high-quality datasets that represent different domains of text:</p>
<ul>
<li><a href="https://huggingface.co/datasets/HuggingFaceFW/fineweb">FineWeb</a> for English content</li>
<li><a href="https://huggingface.co/datasets/HuggingFaceFW/fineweb-2">FineWeb 2</a> for other human languages</li>
<li><a href="https://huggingface.co/datasets/bigcode/starcoderdata">StarCoder</a> for programming languages</li>
</ul>
<p><strong>Per-language metrics</strong>: I sample 2MB of text from each dataset and tokenize it to calculate language-specific performance metrics. This approach has an important limitation: due to UTF-8 encoding differences, 2MB represents vastly different amounts of semantic content across languages. A better approach might compute a global &quot;scaling constant&quot; based on equivalent semantic content—for example, using parallel translations to normalize by the byte size of Harry Potter in English divided by semantic units. As it stands, cross-linguistic comparisons should be interpreted cautiously, and it&#x27;s more reliable to compare different tokenizers on the same language.</p>
<p><strong>Vocabulary metrics</strong>: For analyzing tokenizer vocabularies themselves, I sample 10,000 tokens randomly from each tokenizer&#x27;s vocabulary and analyze their decoded properties.</p>
<p><strong>Language unit definitions</strong>: Different languages structure information differently, so I define &quot;units&quot; for fertility and splitting metrics as follows:</p>
<ul>
<li><strong>Whitespace languages</strong>: tokens per word (space-separated units)</li>
<li><strong>Character-based languages</strong> (e.g., Chinese, Japanese, Thai): tokens per character (excluding whitespace)</li>
<li><strong>Syllable-based languages</strong> (e.g., Tibetan): tokens per syllable (tsheg-separated units, with fallback methods)</li>
</ul>
<h2 id="per-language-metrics-and-results"><a aria-hidden="true" tabindex="-1" href="#per-language-metrics-and-results"><span class="icon icon-link"></span></a>Per-Language Metrics and Results</h2>
<p>Let&#x27;s compare GPT-2, Llama 3, and Kimi K2 on a subset of popular languages to illustrate the kinds of insights tokka-bench can reveal. I&#x27;ve chosen these three to show the evolution of tokenization approaches over time.</p>
<p>Context for each:</p>
<ul>
<li>GPT-2 has a vocab size of ~50K and was released in February 2019</li>
<li>Llama 3 has a vocab size of ~128K and was released in April 2024</li>
<li>Kimi K2 has a vocab size of ~164K and was released in July 2025</li>
</ul>
<h3 id="efficiency-bytes-per-token"><a aria-hidden="true" tabindex="-1" href="#efficiency-bytes-per-token"><span class="icon icon-link"></span></a>Efficiency (Bytes per Token)</h3>
<p><strong><code>bytes_per_token</code></strong>: Average UTF-8 bytes per token (total_bytes / total_tokens). Higher values indicate more efficient compression of text into tokens.</p>
<p><img src="/blog-images/tokka-bench-efficiency.png" alt="Graph of bytes-per-token in multiple languages"/></p>
<p>The efficiency differences reveal training priorities and data composition. Languages with higher bytes-per-token ratios are being compressed more effectively, suggesting either better vocabulary allocation or more training data for vocabulary learning.</p>
<p><strong>Important limitation</strong>: This metric doesn&#x27;t account for UTF-8 encoding differences across scripts. For example, Hindi achieves artificially high efficiency simply because each character requires 3 bytes to encode—allocating just 50 tokens to represent each character in the Hindi alphabet would yield 3 bytes/token efficiency. However, many Hindi characters are formed by combining consonants with vowel signs or consonant clusters, so adding tokens for these combinations (representing 6-9 bytes each) can inflate efficiency metrics while still providing poor semantic coverage. This doesn&#x27;t reflect genuine semantic efficiency. The metric works best for comparing different tokenizers on the same language rather than comparing efficiency across diverse scripts.</p>
<h3 id="coverage-unique-tokens"><a aria-hidden="true" tabindex="-1" href="#coverage-unique-tokens"><span class="icon icon-link"></span></a>Coverage (Unique Tokens)</h3>
<p><strong><code>unique_tokens</code></strong>: Count of distinct token IDs used when encoding sample text in each language. Higher values suggest better coverage of that language&#x27;s script(s) with fewer byte-fallbacks to individual characters.</p>
<p><img src="/blog-images/tokka-bench-coverage.png" alt="Graph of unique tokens in multiple languages"/></p>
<p>I generally find coverage to be the most indicative of the linguistic breakdown of pretraining data. Look at how much higher the Mandarin script coverage is for Kimi K2 than the other tokenizers! This is exactly what we&#x27;d expect, since it&#x27;s a Chinese LLM with vocabulary specifically optimized for Chinese text.</p>
<p>The coverage hierarchy reveals clear training priorities:</p>
<ul>
<li>Chinese has exceptional coverage in Kimi K2</li>
<li>English has the best script coverage by far across all models except second-best in Kimi K2</li>
<li>Latin languages (especially the Romance languages) perform well</li>
<li>Other Latin alphabet languages follow</li>
<li>Korean, Japanese, and Russian show moderate coverage</li>
<li>Hindi, Persian, and Khmer lag significantly behind</li>
</ul>
<p><strong>Note on cross-linguistic comparison</strong>: Since coverage is calculated on fixed 2MB text samples, different languages requiring different numbers of UTF-8 bytes to represent equivalent semantic content makes direct comparison problematic. A more principled approach would calculate coverage as a percentage relative to a normalized baseline—but for now, the metric is most reliable for comparing different tokenizers on the same language rather than comparing coverage across diverse scripts.</p>
<h3 id="word-splitting-rate"><a aria-hidden="true" tabindex="-1" href="#word-splitting-rate"><span class="icon icon-link"></span></a>Word Splitting Rate</h3>
<p><strong><code>word_split_pct</code></strong>: Percentage of units that split into more than one token. Units are defined by language (words for whitespace languages, characters for character-based languages, syllables for syllable-based languages). Lower values generally indicate better alignment with natural unit boundaries.</p>
<p><img src="/blog-images/tokka-bench-word-splitting.png" alt="Graph of word splitting percentage in multiple languages"/></p>
<p>In Mandarin, Kimi K2 has the lowest continued word rate! Only 4% of tokens are continuing a word.</p>
<p><em>Disclaimer: remember, for character-based languages like Mandarin, the metric actually measures per character, not per word. Words in Mandarin can be 1 character or more — most are actually two characters long — but that&#x27;s computationally complex to determine quickly in a benchmark.</em></p>
<h3 id="subword-fertility"><a aria-hidden="true" tabindex="-1" href="#subword-fertility"><span class="icon icon-link"></span></a>Subword Fertility</h3>
<p><strong><code>subword_fertility</code></strong>: Tokens per unit, where units are defined based on language structure (see methodology above). Lower values are better — closer to 1 means fewer pieces per semantic unit.</p>
<p><img src="/blog-images/tokka-bench-subword-fertility.png" alt="Graph of subword fertility in multiple languages"/></p>
<p>In Mandarin, Kimi K2 has the lowest subword fertility! The fertility is below 1, meaning on average each token represents more than 1 character.</p>
<h2 id="vocabulary-metrics-aggregated-across-all-languages"><a aria-hidden="true" tabindex="-1" href="#vocabulary-metrics-aggregated-across-all-languages"><span class="icon icon-link"></span></a>Vocabulary Metrics (Aggregated across All Languages)</h2>
<p>Calculated by sampling tokens from the tokenizer’s vocabulary, then decoding them:</p>
<p><strong><code>tokens_starting_with_space_pct</code></strong>: Share of tokens that decode with a leading space. This reveals both tokenizer design (how much vocabulary is allocated to word beginnings vs. continuations) and training data characteristics (languages without spaces between words will naturally produce lower percentages).</p>
<p><strong><code>tokens_with_whitespace_in_middle_pct</code></strong>: Share of tokens whose decoded text contains whitespace not at the start. Signals multi-word or whitespace-rich tokens that cross natural boundaries.</p>
<p><strong><code>tokens_with_script_overlap_pct</code></strong>: Share of tokens containing characters from multiple Unicode script families. Higher values may indicate mixed-script or byte-level tokens that don&#x27;t respect script boundaries.</p>
<p><strong><code>tokens_with_{script}_unicode_pct</code></strong>: Distribution across scripts (e.g., Latin, Cyrillic, Chinese, Japanese, Korean, Arabic, Devanagari, Thai, Hebrew, Greek, numbers, punctuation, symbols). Shows which writing systems the tokenizer&#x27;s tokens actually cover in practice.</p>
<h2 id="bonus-section-programming-languages"><a aria-hidden="true" tabindex="-1" href="#bonus-section-programming-languages"><span class="icon icon-link"></span></a>Bonus Section: Programming Languages</h2>
<p>Finally, let&#x27;s examine something interesting I noticed with programming languages (we’ll switch from GPT-2 to gpt-oss here):</p>
<p><img src="/blog-images/tokka-bench-coding-efficiency.png" alt="Graph of bytes-per-token in various programming languages"/></p>
<p>There&#x27;s dramatically less variation in efficiency across programming languages — Kimi K2, Llama 3, and GPT-OSS have almost identical bytes-per-token performance in each programming language!</p>
<p>I&#x27;m not entirely sure why this convergence happens, but I find it fascinating. It might indicate shared datasets used by all three models, or perhaps similar proportions of different coding languages in GitHub and other common training sources.</p>
<h2 id="conclusion"><a aria-hidden="true" tabindex="-1" href="#conclusion"><span class="icon icon-link"></span></a>Conclusion</h2>
<p>I hope you find tokka-bench as useful and revealing as I do! There may be some bugs lurking — I&#x27;ve tested a fair bit, but the tool could benefit from much more thorough community testing across diverse languages and use cases.</p>
<p>Please help me by contributing! Whether it&#x27;s bug reports, new metrics, additional tokenizers, or expanded language coverage, community involvement will make this tool much more valuable.</p>
<p>If you&#x27;re from an AI lab with a proprietary model but feel comfortable sharing your tokenizer&#x27;s metrics for informational purposes, please reach out to me! The community would benefit enormously from understanding how state-of-the-art systems handle multilingual tokenization.</p>
<h2 id="acknowledgements-and-references"><a aria-hidden="true" tabindex="-1" href="#acknowledgements-and-references"><span class="icon icon-link"></span></a>Acknowledgements and References</h2>
<ul>
<li><a href="https://howe.vin/">Vin Howe</a>, <a href="https://x.com/s4chinraja">Sachin Raja</a>, and <a href="https://www.linkedin.com/in/jhollowayj/">Jacob Holloway</a> reviewed my post and provided useful feedback</li>
<li><a href="https://www.linkedin.com/in/harsha-vardhan-khurdula-99b400183/">Harsha Vardhan Khurdula</a> helped me gather relevant research and think through metrics systematically</li>
<li>Judit Ács was the first person (as far as I can tell) to introduce subword fertility and proportion of continuation word pieces as standard tokenization metrics in <a href="https://juditacs.github.io/2019/02/19/bert-tokenization-stats.html">this blog post</a></li>
<li>Rust et. al expanded on these ideas in an <a href="https://aclanthology.org/2021.acl-long.243.pdf">ACL paper</a> that was incredibly helpful</li>
</ul>
<h2 id="future-research-ideas"><a aria-hidden="true" tabindex="-1" href="#future-research-ideas"><span class="icon icon-link"></span></a>Future Research Ideas</h2>
<p><strong>Performance correlation</strong>: Which matters more for downstream multilingual performance: tokenization efficiency or vocabulary coverage? The relationship isn&#x27;t immediately obvious and likely varies by task type.</p>
<p><strong>Optimization trade-offs</strong>: How much can you optimize coverage while maintaining efficiency? Is there a Pareto frontier we can characterize mathematically?</p>
<p><strong>Predictive power</strong>: Can we predict multilingual model capabilities from tokenizer metrics alone? If so, this could provide a rapid way to assess model potential before expensive evaluation runs.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>ml/ai</category>
            <category>linguistics</category>
            <category>open-source</category>
        </item>
        <item>
            <title><![CDATA[Nix macOS Starter: Declarative Development Setup with Mise]]></title>
            <link>https://www.bengubler.com/posts/2025-07-08-nix-macos-starter-mise?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2025-07-08-nix-macos-starter-mise</guid>
            <pubDate>Tue, 08 Jul 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Starter Nix config for macOS using nix-darwin, home-manager, and mise.]]></description>
            <content:encoded><![CDATA[<p>I decided to learn Nix on Saturday. After hours of work, I came up with this configuration. Shoutout to my friend <a href="https://github.com/ethanniser">Ethan Niser</a> who gave me the idea and whose config I started out with.</p>
<p>Setting up a new Mac for development is painful. You install Homebrew, Node via nvm, Python via pyenv, configure your shell, install GUI apps individually, and hope you remember everything when switching machines.</p>
<p>Nix makes your entire system configuration declarative and reproducible, but most configurations online are too complex for beginners or assume Linux knowledge.</p>
<p><a href="https://github.com/bgub/nix-macos-starter">nix-macos-starter</a> is a beginner-friendly Nix configuration that includes development tools (mise for runtime management, CLI tools, formatters), GUI applications via Homebrew, and system configuration with sensible defaults.</p>
<p>Replace a few placeholders, run one command, and you have a fully configured development environment.</p>
<h2 id="installation"><a aria-hidden="true" tabindex="-1" href="#installation"><span class="icon icon-link"></span></a>Installation</h2>
<ol>
<li>
<p><strong>Install Nix</strong> using the <a href="https://docs.determinate.systems/#products">Determinate Systems installer</a>. Download the graphical installer for macOS and restart your terminal after installation.</p>
</li>
<li>
<p><strong>Install Homebrew</strong> from <a href="https://brew.sh">brew.sh</a> for GUI applications.</p>
</li>
<li>
<p><strong>Clone and configure</strong>:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="git clone https://github.com/bgub/nix-macos-starter ~/.config/nix
cd ~/.config/nix
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> clone</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> https://github.com/bgub/nix-macos-starter</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> ~/.config/nix</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">cd</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> ~/.config/nix</span></span></code></pre></figure>
</li>
<li>
<p><strong>For Intel Macs</strong>: Change <code>&quot;aarch64-darwin&quot;</code> to <code>&quot;x86_64-darwin&quot;</code> in <code>flake.nix</code> line 28.</p>
</li>
<li>
<p><strong>Replace placeholders</strong> in these files:</p>
<ul>
<li><code>modules/git.nix</code>: <code>YOUR_NAME</code>, <code>YOUR_EMAIL</code>, <code>YOUR_USERNAME</code></li>
<li><code>modules/home-manager.nix</code>: <code>YOUR_USERNAME</code></li>
<li><code>platforms/darwin.nix</code>: <code>YOUR_USERNAME</code> (appears 3 times)</li>
<li><code>hosts/my-macbook/configuration.nix</code>: <code>YOUR_USERNAME</code></li>
</ul>
</li>
<li>
<p><strong>Build and switch</strong>:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="darwin-rebuild switch --flake .#my-macbook
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">darwin-rebuild</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> switch</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --flake</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> .#my-macbook</span></span></code></pre></figure>
</li>
</ol>
<p>After initial setup, use the <code>nix-switch</code> alias to rebuild your configuration.</p>
<h2 id="customization"><a aria-hidden="true" tabindex="-1" href="#customization"><span class="icon icon-link"></span></a>Customization</h2>
<ul>
<li><strong>Add CLI tools</strong>: Edit the <code>packages</code> array in <code>modules/home-manager.nix</code></li>
<li><strong>Add GUI apps</strong>: Edit the <code>casks</code> array in <code>modules/homebrew-common.nix</code></li>
<li><strong>Add development tools</strong>: Add <code>${pkgs.mise}/bin/mise use --global tool@version</code> to the activation script in <code>modules/home-manager.nix</code></li>
<li><strong>Host-specific config</strong>: Use <code>hosts/my-macbook/configuration.nix</code> for machine-specific packages and settings</li>
</ul>
<hr/>
<p><strong>Repository</strong>: <a href="https://github.com/bgub/nix-macos-starter">github.com/bgub/nix-macos-starter</a></p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>open-source</category>
        </item>
        <item>
            <title><![CDATA[Dialects for Humans: Sounding Distinct from LLMs]]></title>
            <link>https://www.bengubler.com/posts/2025-07-01-dialects-for-humans?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2025-07-01-dialects-for-humans</guid>
            <pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Humans are developing new linguistic patterns to distinguish themselves from AI-generated content, and the rate of change will accelerate.]]></description>
            <content:encoded><![CDATA[<h2 id="how-dialects-form"><a aria-hidden="true" tabindex="-1" href="#how-dialects-form"><span class="icon icon-link"></span></a>How Dialects Form</h2>
<p>Dialects often emerge through geographical isolation (think Australian English vs British English). But there&#x27;s another powerful driver of dialect formation: the conscious or unconscious need to signal group affiliation and social identity.</p>
<p>Consider African American Vernacular English (AAVE), Southern American English, or &quot;Valley Girl&quot; speech patterns. These dialects emerged from social dynamics, the human need to belong to a group and distinguish ourselves from others. Now we&#x27;re witnessing the birth of a new dialect divide, between humans and LLMs.</p>
<h2 id="the-llm-dialect-is-real"><a aria-hidden="true" tabindex="-1" href="#the-llm-dialect-is-real"><span class="icon icon-link"></span></a>The LLM Dialect Is Real</h2>
<p>Anyone who spends significant time reading AI-generated content can spot it. Large Language Models have converged on a distinctive writing style that&#x27;s become increasingly recognizable to human readers. Telltale signs include:</p>
<ul>
<li>Em-dashes for dramatic pauses</li>
<li>Formulaic patterns like &quot;It&#x27;s not just X, it&#x27;s Y&quot;</li>
<li>Words like &quot;delve,&quot; &quot;leverage,&quot; and &quot;nuanced&quot;</li>
<li>Numbered lists and bullet points</li>
</ul>
<p>This convergence across different SOTA models is no surprise. The highly-weighted content that shapes these models (books, Wikipedia articles, news, academic papers) overlaps significantly across training sets and creates a shared dialect, which I call &quot;LLM English&quot;.</p>
<h2 id="humans-are-adapting"><a aria-hidden="true" tabindex="-1" href="#humans-are-adapting"><span class="icon icon-link"></span></a>Humans Are Adapting</h2>
<p>Writers like me who previously used em-dashes liberally now find themselves switching to double dashes (&quot;--&quot;) or avoiding the punctuation entirely. The characteristic LLM juxtaposition style feels suddenly artificial when we write it ourselves. Numbered lists and excessive bolding now carry the stigma of AI generation.</p>
<h2 id="a-new-dialect-human-english"><a aria-hidden="true" tabindex="-1" href="#a-new-dialect-human-english"><span class="icon icon-link"></span></a>A New Dialect: &quot;Human English&quot;</h2>
<p>LLMs generate content by predicting the most likely next tokens based on their training data. Patterns and phrases that weren&#x27;t present in their pretraining data can be understood when encountered, but are unlikely to be spontaneously generated.</p>
<p>If human communities can rapidly cycle through dialectical innovations like new slang, novel grammatical constructions, and fresh idiomatic expressions, they can stay ahead of the training curve. LLMs will always be working with data that&#x27;s months or years behind the cutting edge of human linguistic creativity.</p>
<p>Consider how quickly internet slang changes. By the time &quot;yeet&quot; made it into dictionaries, Gen Z had already moved on to newer expressions. This rapid evolution could become even more pronounced as a conscious strategy for maintaining human linguistic identity.</p>
<h2 id="technical-challenges-of-differentiation"><a aria-hidden="true" tabindex="-1" href="#technical-challenges-of-differentiation"><span class="icon icon-link"></span></a>Technical Challenges of Differentiation</h2>
<p>There is one significant technical hurdle to this strategy: context length. Modern LLMs like Gemini can handle extremely long contexts, enough to load thousands of recent tweets as few-shot examples. An AI system could theoretically observe contemporary human dialect patterns in real-time and incorporate them into responses.</p>
<p>However, this type of real-time dialectical mimicry would be computationally expensive. Though technically possible, the cost-benefit analysis makes it unlikely for most applications.</p>
<h2 id="summary-the-future-of-english"><a aria-hidden="true" tabindex="-1" href="#summary-the-future-of-english"><span class="icon icon-link"></span></a>Summary: The Future of English</h2>
<p>Dialects emerge when there are strong social incentives for signaling group membership and distinguishing in-groups from out-groups. The LLM revolution has created exactly these conditions.</p>
<p>We now have clear social value in demonstrating our humanity through our communication patterns. Consciously or unconsciously, people are developing new ways to signal &quot;I am human&quot; through their writing and speech.</p>
<p>I predict we&#x27;ll see the emergence of distinct &quot;human English&quot; dialects that evolve rapidly to stay ahead of AI capabilities. These dialects will include avoidance of AI-like patterns in addition to positive innovations in slang, grammar, and idioms.</p>
<h2 id="research-questions"><a aria-hidden="true" tabindex="-1" href="#research-questions"><span class="icon icon-link"></span></a>Research Questions</h2>
<ul>
<li>How successfully can AI systems replicate unique dialect variants when given relatively small example sets?</li>
<li>In which online communities is &quot;human English&quot; already emerging? Are there early adopter communities to study?</li>
<li>How quickly do human dialectical innovations spread through online communities? (I&#x27;m sure this has been studied, I just haven&#x27;t researched it thoroughly)</li>
</ul>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>ml/ai</category>
            <category>linguistics</category>
        </item>
        <item>
            <title><![CDATA[No, Children Aren't Naturally Better at Languages]]></title>
            <link>https://www.bengubler.com/posts/2024-08-22-children-arent-naturally-better-at-languages?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2024-08-22-children-arent-naturally-better-at-languages</guid>
            <pubDate>Thu, 22 Aug 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[A short summary of Mark Rosenfelder's thoughts on child language acquisition]]></description>
            <content:encoded><![CDATA[<p>I just finished Mark Rosenfelder&#x27;s excellent article entitled &quot;<a href="https://www.zompist.com/whylang.html">When do people learn languages?</a>&quot;, in which he spends an entire section debunking a widespread myth: that <strong>children learn languages easily</strong>. I&#x27;ve long disagreed with that claim, so I was happy to come across his article.</p>
<p>Here&#x27;s a summary of his arguments:</p>
<ul>
<li>&quot;Children begin learning languages at birth... and haven&#x27;t really mastered it subtleties before the age of ten years&quot;</li>
<li>Language learning isn&#x27;t effortless for children: &quot;children don&#x27;t learn a language if they can get away with not learning it&quot;</li>
<li>&quot;A child is likely to end up as a fluent speaker of a language only if there are significant people in her life who speak it&quot;</li>
<li>&quot;It&#x27;s a myth that children learn to speak mainly from their parents. They don&#x27;t: they learn mostly from their peers&quot;</li>
</ul>
<p>He mentions that many people believe children to learn language better than adults, then refutes this idea.</p>
<p>&quot;One may fall back on the position that language may be hard for children to learn, but at least they do it <strong>better than adults</strong>. This, however, turns out to be surprisingly difficult to prove. Singleton examined hundreds of studies, and found them resoundingly ambiguous. Quite a few studies, in fact, find that adult learners progress <strong>faster than children</strong> .... Even in phonetics, sometimes the last stronghold of the kids-learn-free position, there are studies finding that adults are better at recognizing and producing foreign sounds.&quot;</p>
<p>And he mentions a few reasons that children learn languages so well:</p>
<ul>
<li>&quot;They can devote almost their full time to it. Adults consider half an hour&#x27;s study a day to be onerous.&quot;</li>
<li>&quot;Their motivation is intense .... children can get very little of what they want without learning language(s).&quot;</li>
<li>&quot;Their peers are nastier. Embarrassment is a prime motivating factor for human beings&quot;</li>
</ul>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>linguistics</category>
        </item>
        <item>
            <title><![CDATA[More Language Thoughts]]></title>
            <link>https://www.bengubler.com/posts/2024-08-22-more-language-thoughts?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2024-08-22-more-language-thoughts</guid>
            <pubDate>Thu, 22 Aug 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Quick thoughts about miscellaneous language / linguistic topics]]></description>
            <content:encoded><![CDATA[<p>See <a href="https://x.com/fusaroli/status/1309454176800342016?lang=en">this thread on X</a> about peculiarities of Danish. <strong>I&#x27;m pretty skeptical of claims that &quot;all languages are equally complex&quot;</strong>, partially because of research like this. Interesting findings:</p>
<ul>
<li>&quot;Danish has an unusual speech opacity (consonant reduction)&quot;</li>
<li>&quot;Danish children do present delays in language acquisition: At 15 months, Danish children possess a median vocabulary of 90 words, compared to 140 for Norwegian kids and 150 for Swedish ones&quot;</li>
<li>&quot;Up to 8 years of age, Danish children have more difficulties [compared to other Scandinavian children] with inflectional morphology, e.g. declining regular and irregular verbs in simple past&quot;</li>
<li>&quot;Adult native speakers of Danish do NOT seem to have issues with Danish&quot;</li>
<li>&quot;Danish native speakers are equally affected by near and distant sentential context in their disambiguation of words and phonemes, while Norwegians just won&#x27;t wait for distant contexts and will make their decisions earlier.&quot;</li>
<li>&quot;The speech signal does not get clearer as Danish native speakers grow up, so we hypothesized that they learn to put increased focus on additional sources of information (e.g. context)&quot;</li>
</ul>
<p>They ran a nifty experiment to demonstrate this. Two cool findings:</p>
<ul>
<li>&quot;Danish native speakers are equally affected by near and distant sentential context in their disambiguation of words and phonemes, while Norwegians just won&#x27;t wait for distant contexts and will make their decisions earlier.&quot;</li>
<li>&quot;Forcing them to wait makes them more similar to Danish native speakers&quot;</li>
</ul>
<p>I love learning about stuff like this. If you do too, try reading about the difference in rates of dyslexia across languages!</p>
<p>One more thing: check out the Duostories for <a href="https://duostories.org/tok-en">Toki Pona</a> and <a href="https://duostories.org/tok2-en">Toki Pona Glyphs</a> (sitelen pona!)</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>linguistics</category>
        </item>
        <item>
            <title><![CDATA[Links #1]]></title>
            <link>https://www.bengubler.com/posts/2024-08-14-links-1?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2024-08-14-links-1</guid>
            <pubDate>Wed, 14 Aug 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Interesting things I've been reading]]></description>
            <content:encoded><![CDATA[<p>First, interesting things that I&#x27;ve read recently:</p>
<ul>
<li>In <a href="https://www.nytimes.com/2024/07/27/us/jd-vance-emails-transgender-classmate-highlights.html">5 Excerpts From JD Vance’s Emails to a Transgender Classmate</a>, Vance comes off as a fairly moderate and thoughtful person — comforting if he ends up becoming VP.</li>
<li>I found <a href="https://www.theatlantic.com/podcasts/archive/2024/08/one-israeli-hostages-unusual-experience-in-gaza/679318/">One Israeli Hostage’s Unusual Experience in Gaza</a> fascinating — particularly Atzili&#x27;s description of her captors. She described them as educated, English-speaking, highly religious members of Hamas who seemed protective and decent, and who hadn&#x27;t known about the looting or taking of female captives.</li>
<li>The Wikipedia article about <a href="https://en.wikipedia.org/wiki/Kurrent">Kurrent</a>, an old style of German cursive.</li>
<li>A <a href="https://x.com/jxmnop/status/1816958426385383753">Tweet</a> about training GPT-2 to multiply better by starting out with CoT and then removing tokens.</li>
<li><a href="https://www.robkhenderson.com/p/no-one-expects-young-men-to-do-anything">No One Expects Young Men To Do Anything and They Are Responding By Doing Nothing</a>. I enjoy Henderson&#x27;s writing on &quot;luxury beliefs&quot;, and he points to something that I think is super important. Strong social norms can seem repressive but often make life better, especially for those who are predisposed by their genes or environments to bad choices.</li>
<li><a href="https://en.wikipedia.org/wiki/Linear_B">The Life of Michael Ventris</a>, who deciphered Linear B. (I didn&#x27;t know Linear B was used to write Greek until after reading!)</li>
<li><a href="https://www.deseret.com/magazine/2024/07/15/ray-epps-stolen-election-jan-6/">The undercover agent who wasn’t</a> chronicles the story of James Epps, who was accused of being a &quot;false flag&quot; government operative and instigating the Jan. 6 attack on the Capitol. Very instructive and fascinating to see how quickly conspiracy theories can grow and become weaponized even against their adherents.</li>
<li><a href="https://www.theatlantic.com/ideas/archive/2024/08/america-has-too-many-laws-neil-gorsuch/679237/">America Has Too Many Laws</a>
<blockquote>
<p>If you were to sit down and read through all of our criminal laws and regulations—or at least flip through them—you would find plenty of surprises. You would learn, for example, that it’s a federal crime to damage a government-owned lamp in Washington, D.C.; consult with a known pirate; or advertise wine by suggesting its intoxicating qualities.</p>
</blockquote>
</li>
<li><a href="https://www.deseret.com/family/2024/08/17/pacific-islander-tonga-community-culture-faith-utah/">A reunion, a vigil and reasons to celebrate</a> discusses the life of Pacific Islanders in the U.S. and especially Utah. Interesting quote from the article:<!-- -->
<blockquote>
<p>Feltch-Malohifo’ou believes some behavior problems could be addressed if officials understood Pacific Island culture better. “I’ve told judges that if you put a mother on the stand, a church leader, an elder and they would have to do community service for the offender, you would never have anybody else go back to jail. Because they are not going to let their mother paint graffiti off a wall. I’ve told judges they’ve got to use the cultural things that matter,” she said.</p>
</blockquote>
</li>
<li><a href="https://www.theatlantic.com/newsletters/archive/2024/08/the-case-for-choosing-death-not-immortality/679400/">The Case for Choosing Death, Not Immortality</a></li>
</ul>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>random</category>
        </item>
        <item>
            <title><![CDATA[Using HuggingFace Datasets Offline]]></title>
            <link>https://www.bengubler.com/posts/2024-07-16-hf-datasets-offline?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2024-07-16-hf-datasets-offline</guid>
            <pubDate>Tue, 16 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[How to save a HuggingFace dataset to disk and use it offline]]></description>
            <content:encoded><![CDATA[<p>This is pretty simple, but quite helpful if you&#x27;re running jobs on a compute node that doesn&#x27;t have internet access.</p>
<p>On the login node or another machine with internet access, run the following Python code:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="python" data-theme="github-dark-dimmed github-light" __raw_string__="import datasets

x = datasets.load_dataset(&quot;my_dataset&quot;)

x.save_to_disk(&quot;./my_dataset_local&quot;)
"><code data-language="python" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> datasets</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">x </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> datasets.load_dataset(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;my_dataset&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">x.save_to_disk(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;./my_dataset_local&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>Then, if needed, copy the files to the machine running your job. Now, from that offline machine, loading the dataset is simple!</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="python" data-theme="github-dark-dimmed github-light" __raw_string__="y = datasets.load_from_disk(&quot;./hellaswag_local&quot;)
y # DatasetDict({...})
"><code data-language="python" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">y </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> datasets.load_from_disk(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;./hellaswag_local&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">y </span><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># DatasetDict({...})</span></span></code></pre></figure>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>ml/ai</category>
        </item>
        <item>
            <title><![CDATA[Tips #1]]></title>
            <link>https://www.bengubler.com/posts/2024-07-16-tips-1?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2024-07-16-tips-1</guid>
            <pubDate>Tue, 16 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Markdown detection in Google Docs, swiping between tabs in Brave Browser for iOS, and running TypeScript files from the command line.]]></description>
            <content:encoded><![CDATA[<ul>
<li>You can turn on Markdown detection in Google Docs! Just go to <code>Tools &gt; Preferences &gt; Automatically detect Markdown</code></li>
<li>Brave Browser for iOS supports swiping left and right between between tabs — you just have to swipe <em>below</em> the URL bar.</li>
<li>If you need to run a <code>.ts</code> file from the command-line, use Bun instead of ts-node! It&#x27;s much simpler and doesn&#x27;t have all the weird issues with the package.json <code>&quot;type&quot;</code> field.</li>
</ul>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>random</category>
            <category>ml/ai</category>
        </item>
        <item>
            <title><![CDATA[Making a Radix Dropdown the Same Width as Its Trigger]]></title>
            <link>https://www.bengubler.com/posts/2024-07-02-radix-shadcn-dropdown-trigger-width?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2024-07-02-radix-shadcn-dropdown-trigger-width</guid>
            <pubDate>Tue, 02 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[How to make the dropdown content of a Radix or shadcn dropdown match the width of the trigger.]]></description>
            <content:encoded><![CDATA[<p>My website uses the <a href="https://ui.shadcn.com/docs/components/dropdown-menu">shadcn Dropdown Menu</a> (based on Radix&#x27;s <code>DropdownMenuPrimitive</code>) to allow users to change the current website theme.</p>
<p>By default, the content of a dropdown component isn&#x27;t the same width as its trigger. That bugged me, and so I spent a while looking for a solution. Finally, I found the answer <a href="https://www.radix-ui.com/primitives/docs/components/dropdown-menu#constrain-the-contentsub-content-size">in Radix&#x27;s docs</a>!</p>
<p>Put the below code in your CSS file (you can remove the <code>@layer utilities</code> wrapper if not using Tailwind):</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="css" data-theme="github-dark-dimmed github-light" __raw_string__="@layer utilities {
  .dropdown-content-width-full {
    width: var(--radix-dropdown-menu-trigger-width);
    max-height: var(--radix-dropdown-menu-content-available-height);
  }
}
"><code data-language="css" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">@layer</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> utilities {</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#6F42C1">  .dropdown-content-width-full</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">    width</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">var</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">--radix-dropdown-menu-trigger-width</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">    max-height</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">var</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">--radix-dropdown-menu-content-available-height</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Now, change your <code>&lt;DropdownMenuComponent&gt;</code> to look like this:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="jsx" data-theme="github-dark-dimmed github-light" __raw_string__="&lt;DropdownMenuContent align=&quot;end&quot; className=&quot;dropdown-content-width-full&quot;&gt;
"><code data-language="jsx" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">&lt;</span><span style="--shiki-dark:#8DDB8C;--shiki-light:#005CC5">DropdownMenuContent</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#6F42C1"> align</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;end&quot;</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#6F42C1"> className</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;dropdown-content-width-full&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">&gt;</span></span></code></pre></figure>
<p>Voila! The content and trigger are the same width. You&#x27;re welcome.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>frontend</category>
        </item>
        <item>
            <title><![CDATA[Debugging Python in VSCode]]></title>
            <link>https://www.bengubler.com/posts/2024-07-02-debugging-python-vscode?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2024-07-02-debugging-python-vscode</guid>
            <pubDate>Tue, 02 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Using the code.interact() function to launch an interactive code interpreter]]></description>
            <content:encoded><![CDATA[<p>They say you learn something new every day, but I was shocked to learn about a fantastic Python feature I had no idea about.</p>
<p>Apparently, you can pause execution of a Python file and open up an interactive terminal with the local variables! I discovered this while watching Andrej Karpathy&#x27;s video about <a href="https://www.youtube.com/watch?v=l8pRSuU81PU">reproducing GPT-2</a> (trust Karpathy to know about random tricks like this.)</p>
<p>Just <code>import code</code> at the top of your file, then put <code>code.interact(local=locals())</code> at any place in your code. Voila! When execution reaches that point, a Python interpreter will open in the terminal with access to all local variables. You can press <code>CTRL+D</code> to close the interpreter and continue execution, or type <code>quit()</code> to quit the terminal and stop execution.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>random</category>
        </item>
        <item>
            <title><![CDATA[On Microblogging]]></title>
            <link>https://www.bengubler.com/posts/2024-07-01-on-microblogging?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2024-07-01-on-microblogging</guid>
            <pubDate>Mon, 01 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Why I'm starting a microblog and how it differs from my main blog.]]></description>
            <content:encoded><![CDATA[<p>I&#x27;m a deep believer in the power of writing as a tool for thought. I often find that I don&#x27;t truly understand a concept until after I&#x27;ve written a thorough explanation. Writing also helps me remember ideas better and apply them into my life more effectively.</p>
<p>I&#x27;m also a deep believer in the power of sharing knowledge. As a computer programmer, I want to share bug fixes and hacks I&#x27;ve discovered. As a mathematician, I want to share interesting insights about new concepts I&#x27;ve learned. As a language learner, I want to tell others about fascinating historical details or words in various languages. And as a human, I want to connect with other people and have the opportunity to be heard.</p>
<p>Despite the importance of writing, I&#x27;m quite busy. I don&#x27;t have time to write a polished blog post about every topic I want to share. Nor do I care to refine my writing extensively for public consumption.</p>
<p>Thus, I&#x27;m starting a microblog. Here I&#x27;ll write about any topic that interests me — math, computer science, faith, philosophy, languages, etc. I may also share external links, book reviews, or interesting personal experiences. My tone will range from formal to casual as I please.</p>
<p>Speaking of tone, I&#x27;m placing one constraint on my microblog: I won&#x27;t allow myself to use LLMs in authoring posts. This should force me to maintain my writing skills, provide me with an archive of content that is mine alone, and reflect my personality more genuinely.</p>
<p>I hope you enjoy reading! If you&#x27;re an RSS user, you can subscribe at <a href="https://bengubler.com/rss.xml?type=microblog">/rss.xml?type=microblog</a>.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>random</category>
        </item>
        <item>
            <title><![CDATA[Rebuilding Alpaca with the Hugging Face Trainer Class]]></title>
            <link>https://www.bengubler.com/posts/2023-11-07-rebuilding-alpaca-huggingface-trainer?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2023-11-07-rebuilding-alpaca-huggingface-trainer</guid>
            <pubDate>Tue, 07 Nov 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Fine-tuning Llama-2-7B using the Alpaca dataset and Hugging Face Trainer]]></description>
            <content:encoded><![CDATA[<p><em>UPDATE 2024: The code in this post may be outdated. I recommend checking the <a href="https://huggingface.co/docs/transformers/v4.41.3/en/trainer">Hugging Face Trainer documentation</a> for the most up-to-date information.</em></p>
<h2 id="introduction"><a aria-hidden="true" tabindex="-1" href="#introduction"><span class="icon icon-link"></span></a>Introduction</h2>
<p>In March of this year (2023), a lab at Stanford released a small project that quickly became massively influential — <a href="https://crfm.stanford.edu/2023/03/13/alpaca.html">Alpaca</a>. The authors used <code>text-davinci-003</code> (an InstructGPT model from OpenAI) to generate a dataset with 52K examples of prompts and responses, then fine-tuned Llama-7B using those prompt and response pairs.</p>
<p>The result was surprisingly good — Alpaca was able to interact with users similarly to OpenAI&#x27;s InstructGPT models, despite being inexpensive to train and not using a human-created training dataset. In this blog post, we&#x27;ll write code to train our own model from scratch using the Alpaca dataset.</p>
<p><em>The code in this blog post is based on that in the <a href="https://github.com/tatsu-lab/stanford_alpaca">Alpaca repo</a>, though my hope is that it should be simpler and more intuitive. All credit should go to the original authors of the paper.</em></p>
<h2 id="setup"><a aria-hidden="true" tabindex="-1" href="#setup"><span class="icon icon-link"></span></a>Setup</h2>
<p>You&#x27;ll need to install <code>torch</code>, <code>transformers</code>, <code>datasets</code>, and <code>accelerate</code>. <code>wandb</code> is great if you want to track training loss over time. And, of course, you&#x27;ll need some good GPUs if you want your model to train quickly.</p>
<p>Start out by creating one main folder, <code>alpaca-repro</code>, with two subfolders: one called <code>trainer</code>, where your training code will go, and one called <code>finetunes</code>, where we&#x27;ll save your fine-tuned model.</p>
<h2 id="step-1-loading-and-processing-the-data"><a aria-hidden="true" tabindex="-1" href="#step-1-loading-and-processing-the-data"><span class="icon icon-link"></span></a>Step 1: Loading and Processing the Data</h2>
<p>Put all of the code in this section into <code>trainer/get_data.py</code>.</p>
<p>We&#x27;ll begin by loading the <a href="https://huggingface.co/datasets/tatsu-lab/alpaca">Alpaca data</a> from the Hugging Face hub. Each question/prompt pair in the dataset needs to be converted into a single string that we can train the model on, but we actually generate one extra string: <code>source</code>, which we use further down to ignore labels so our model doesn&#x27;t train on instructions.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="python" data-theme="github-dark-dimmed github-light" __raw_string__="from datasets import load_dataset

original_dataset = load_dataset(&quot;tatsu-lab/alpaca&quot;)[&quot;train&quot;]

template_no_context = &quot;&quot;&quot;Below is an instruction that describes a task. \
Write a response that appropriately completes the request.

### Instruction:
{instruction}

### Response:
&quot;&quot;&quot;

template_context = &quot;&quot;&quot;Below is an instruction that describes a task. \
Write a response that appropriately completes the request.

### Instruction:
{instruction}

### Input:
{input}

### Response:
&quot;&quot;&quot;

def data_to_string(data):

    instruction = data[&quot;instruction&quot;]
    context = data[&quot;input&quot;]
    response = data[&quot;output&quot;]

    template = template_context if len(context) &gt; 0 else template_no_context
    source = template.format(instruction=instruction, input=context)

    return {
        &quot;source&quot;: source,
        &quot;text&quot;: source + response,
    }


dataset = original_dataset.map(
    data_to_string
).remove_columns([&#x27;instruction&#x27;, &#x27;input&#x27;, &#x27;output&#x27;])
"><code data-language="python" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> datasets </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> load_dataset</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">original_dataset </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> load_dataset(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;tatsu-lab/alpaca&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)[</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;train&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">]</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">template_no_context </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;&quot;&quot;Below is an instruction that describes a task. </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">\</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">Write a response that appropriately completes the request.</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">### Instruction:</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#005CC5">{instruction}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">### Response:</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;&quot;&quot;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">template_context </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;&quot;&quot;Below is an instruction that describes a task. </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">\</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">Write a response that appropriately completes the request.</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">### Instruction:</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#005CC5">{instruction}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">### Input:</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#005CC5">{input}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">### Response:</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;&quot;&quot;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">def</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1"> data_to_string</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(data):</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    instruction </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> data[</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;instruction&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    context </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> data[</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;input&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    response </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> data[</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;output&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">]</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    template </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> template_context </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> len</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(context) </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">&gt;</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> else</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> template_no_context</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    source </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> template.format(</span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">instruction</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">instruction, </span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">input</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">context)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">        &quot;source&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: source,</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">        &quot;text&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: source </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> response,</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">dataset </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> original_dataset.map(</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    data_to_string</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">).remove_columns([</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&#x27;instruction&#x27;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&#x27;input&#x27;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&#x27;output&#x27;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">])</span></span></code></pre></figure>
<p>Here we split the data so we can use 10% for evaluation and tests later on.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="python" data-theme="github-dark-dimmed github-light" __raw_string__="processed_dataset = dataset.train_test_split(test_size=0.1)

train_dataset = processed_dataset[&quot;train&quot;]
eval_dataset = processed_dataset[&quot;test&quot;]
"><code data-language="python" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">processed_dataset </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> dataset.train_test_split(</span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">test_size</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">0.1</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">train_dataset </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> processed_dataset[</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;train&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">eval_dataset </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> processed_dataset[</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;test&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">]</span></span></code></pre></figure>
<p>Finally, we define a data collator to be used by our training loop. Remember that each <code>text</code> string is just made up of the <code>source</code> plus the response. So we tokenize the <code>source</code> string to figure out how many labels in the <code>text</code> string to ignore.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="python" data-theme="github-dark-dimmed github-light" __raw_string__="IGNORE_TOKEN = -100

def data_collator(features, tokenizer):
    sources = [feature[&quot;source&quot;] for feature in features]
    targets = [feature[&quot;text&quot;] for feature in features]

    source_tokens = tokenizer(
        sources,
        return_tensors=&quot;pt&quot;,
        padding=&#x27;longest&#x27;,
        max_length=None,
    )

    target_tokens = tokenizer(
        targets,
        return_tensors=&quot;pt&quot;,
        padding=&#x27;longest&#x27;,
        max_length=None,
    )

    labels = target_tokens[&quot;input_ids&quot;].clone()

    for i in range(len(labels)):
        source_len = source_tokens[&quot;attention_mask&quot;][i].sum()

        labels[i, :source_len] = IGNORE_TOKEN

    res = {
        &quot;input_ids&quot;: target_tokens[&quot;input_ids&quot;],
        &quot;attention_mask&quot;: target_tokens[&quot;attention_mask&quot;],
        &quot;labels&quot;: labels,
    }

    return res
"><code data-language="python" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">IGNORE_TOKEN</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">100</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">def</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1"> data_collator</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(features, tokenizer):</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    sources </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> [feature[</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;source&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">for</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> feature </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">in</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> features]</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    targets </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> [feature[</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;text&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">for</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> feature </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">in</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> features]</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    source_tokens </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> tokenizer(</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">        sources,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">        return_tensors</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;pt&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">        padding</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&#x27;longest&#x27;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">        max_length</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">None</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    )</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    target_tokens </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> tokenizer(</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">        targets,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">        return_tensors</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;pt&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">        padding</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&#x27;longest&#x27;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">        max_length</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">None</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    )</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    labels </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> target_tokens[</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;input_ids&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">].clone()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">    for</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> i </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">in</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> range</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">len</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(labels)):</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">        source_len </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> source_tokens[</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;attention_mask&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">][i].sum()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">        labels[i, :source_len] </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> IGNORE_TOKEN</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    res </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">        &quot;input_ids&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: target_tokens[</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;input_ids&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">        &quot;attention_mask&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: target_tokens[</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;attention_mask&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">        &quot;labels&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: labels,</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> res</span></span></code></pre></figure>
<h2 id="step-2-writing-our-training-loop"><a aria-hidden="true" tabindex="-1" href="#step-2-writing-our-training-loop"><span class="icon icon-link"></span></a>Step 2: Writing Our TrAIning Loop</h2>
<p>Put all of the code in this section into <code>trainer/loop.py</code>.</p>
<p>This code is fairly self-explanatory, so I&#x27;ve just annotated it with comments.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="python" data-theme="github-dark-dimmed github-light" __raw_string__="from transformers import LlamaForCausalLM, LlamaTokenizer, Trainer, TrainingArguments
from accelerate import Accelerator
from get_data import train_dataset, eval_dataset, data_collator

accelerator = Accelerator()

MODEL_PATH = &quot;meta-llama/Llama-2-7b-hf&quot; # path to Llama on Hugging Face Hub
OUTPUT_DIR = &quot;../finetunes/alpaca-7b&quot; # where to save the fine-tuned model

tokenizer = LlamaTokenizer.from_pretrained(MODEL_PATH, legacy=False)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = &quot;right&quot; # not set by default, strangely

model = LlamaForCausalLM.from_pretrained(
    MODEL_PATH, device_map=&quot;auto&quot;
)

training_args = TrainingArguments(
    output_dir=&#x27;checkpoints&#x27;, # where Trainer will save model checkpoints
    num_train_epochs=1, # start with a low number of epochs for testing
    learning_rate=2e-5,
    logging_steps=10,
    per_device_train_batch_size=8,
    remove_unused_columns=False,
    save_steps=1000,
    save_total_limit=1,
    report_to=&quot;wandb&quot;,
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
    tokenizer=tokenizer,
    data_collator=lambda x: data_collator(x, tokenizer),
)

trainer.train()
trainer.evaluate()

model.save_pretrained(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)
"><code data-language="python" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> transformers </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> LlamaForCausalLM, LlamaTokenizer, Trainer, TrainingArguments</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> accelerate </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> Accelerator</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> get_data </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> train_dataset, eval_dataset, data_collator</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">accelerator </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> Accelerator()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">MODEL_PATH</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;meta-llama/Llama-2-7b-hf&quot;</span><span style="--shiki-dark:#768390;--shiki-light:#6A737D"> # path to Llama on Hugging Face Hub</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">OUTPUT_DIR</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;../finetunes/alpaca-7b&quot;</span><span style="--shiki-dark:#768390;--shiki-light:#6A737D"> # where to save the fine-tuned model</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">tokenizer </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> LlamaTokenizer.from_pretrained(</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">MODEL_PATH</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">legacy</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">False</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">tokenizer.pad_token </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> tokenizer.eos_token</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">tokenizer.padding_side </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;right&quot;</span><span style="--shiki-dark:#768390;--shiki-light:#6A737D"> # not set by default, strangely</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">model </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> LlamaForCausalLM.from_pretrained(</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">    MODEL_PATH</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">device_map</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;auto&quot;</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">training_args </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> TrainingArguments(</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    output_dir</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&#x27;checkpoints&#x27;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># where Trainer will save model checkpoints</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    num_train_epochs</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># start with a low number of epochs for testing</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    learning_rate</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">2e-5</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    logging_steps</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">10</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    per_device_train_batch_size</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">8</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    remove_unused_columns</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">False</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    save_steps</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">1000</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    save_total_limit</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    report_to</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;wandb&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">trainer </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> Trainer(</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    model</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">model,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    args</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">training_args,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    train_dataset</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">train_dataset,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    eval_dataset</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">eval_dataset,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    tokenizer</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">tokenizer,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    data_collator</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=lambda</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> x: data_collator(x, tokenizer),</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">trainer.train()</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">trainer.evaluate()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">model.save_pretrained(</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">OUTPUT_DIR</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">tokenizer.save_pretrained(</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">OUTPUT_DIR</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span></code></pre></figure>
<h2 id="step-3-running-our-training-loop"><a aria-hidden="true" tabindex="-1" href="#step-3-running-our-training-loop"><span class="icon icon-link"></span></a>Step 3: Running Our TrAIning Loop</h2>
<p>Create <code>trainer/accelerate_config.yaml</code>, and paste in the following configuration:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="yaml" data-theme="github-dark-dimmed github-light" __raw_string__="compute_environment: LOCAL_MACHINE
deepspeed_config: {}
distributed_type: &quot;NO&quot;
downcast_bf16: &quot;no&quot;
machine_rank: 0
main_process_ip: null
main_process_port: null
main_training_function: main
mixed_precision: &quot;no&quot;
num_machines: 1
num_processes: 1
use_cpu: false
"><code data-language="yaml" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">compute_environment</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">LOCAL_MACHINE</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">deepspeed_config</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: {}</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">distributed_type</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;NO&quot;</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">downcast_bf16</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;no&quot;</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">machine_rank</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">0</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">main_process_ip</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">null</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">main_process_port</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">null</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">main_training_function</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">main</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">mixed_precision</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;no&quot;</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">num_machines</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">1</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">num_processes</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">1</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">use_cpu</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">false</span></span></code></pre></figure>
<p>Then <code>cd</code> into <code>./trainer</code> and run:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="accelerate launch --config_file accelerate_config.yaml loop.py
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">accelerate</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> launch</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --config_file</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> accelerate_config.yaml</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> loop.py</span></span></code></pre></figure>
<p>Saving the model and weights might take a while, so be patient!</p>
<h2 id="step-4-testing-our-fine-tuned-model"><a aria-hidden="true" tabindex="-1" href="#step-4-testing-our-fine-tuned-model"><span class="icon icon-link"></span></a>Step 4: Testing Our Fine-Tuned Model!</h2>
<p>I wrote a simple script to load up our fine-tuned model and interact with it! It doesn&#x27;t support conversations with context, but it&#x27;s a great way to see how the model is working.</p>
<p>Create a new file called <code>alpaca-repro/model_test.py</code>, then run <code>python3 model_test.py</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="python" data-theme="github-dark-dimmed github-light" __raw_string__="from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline

template = &quot;&quot;&quot;Below is an instruction that describes a task. \
Write a response that appropriately completes the request.

### Instruction:
{instruction}

### Response:
&quot;&quot;&quot;

model_path = &quot;./finetunes/alpaca-7b&quot;

tokenizer = AutoTokenizer.from_pretrained(model_path, legacy=False)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = &quot;right&quot;

model = AutoModelForCausalLM.from_pretrained(
    model_path, device_map=&quot;auto&quot;, local_files_only=True
)

pipe = pipeline(
    &quot;text-generation&quot;,
    model=model,
    tokenizer=tokenizer,
    return_full_text=False,
    do_sample=True,
    temperature=0.9,
    max_new_tokens=200,
)

def prompt_model():
    prompt = input(&quot;Enter your question: &quot;)
    prompt = template.format(instruction=prompt)
    answer = pipe(prompt)
    print(answer[0][&quot;generated_text&quot;])

while True:
    prompt_model()
"><code data-language="python" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> transformers </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> AutoTokenizer, AutoModelForCausalLM, pipeline</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">template </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;&quot;&quot;Below is an instruction that describes a task. </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">\</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">Write a response that appropriately completes the request.</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">### Instruction:</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#005CC5">{instruction}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">### Response:</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;&quot;&quot;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">model_path </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;./finetunes/alpaca-7b&quot;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">tokenizer </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> AutoTokenizer.from_pretrained(model_path, </span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">legacy</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">False</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">tokenizer.pad_token </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> tokenizer.eos_token</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">tokenizer.padding_side </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> &quot;right&quot;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">model </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> AutoModelForCausalLM.from_pretrained(</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    model_path, </span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">device_map</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;auto&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">, </span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">local_files_only</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">True</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">pipe </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> pipeline(</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">    &quot;text-generation&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    model</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">model,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    tokenizer</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">tokenizer,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    return_full_text</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">False</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    do_sample</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">True</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    temperature</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">0.9</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">    max_new_tokens</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">200</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">def</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1"> prompt_model</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">():</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    prompt </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> input</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;Enter your question: &quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    prompt </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> template.format(</span><span style="--shiki-dark:#F69D50;--shiki-light:#E36209">instruction</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">prompt)</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    answer </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> pipe(prompt)</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">    print</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(answer[</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">][</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;generated_text&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">])</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">while</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> True</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    prompt_model()</span></span></code></pre></figure>
<h2 id="conclusion"><a aria-hidden="true" tabindex="-1" href="#conclusion"><span class="icon icon-link"></span></a>Conclusion</h2>
<p>I hope this article was helpful and informative! My plan is to follow it up in a few days with an explanation of how to use FSDP with the Hugging Face Trainer.</p>
<p>If you got mixed up along the way, here&#x27;s a Gist with the final project code: <a href="https://gist.github.com/bgub/1da2c0064d53decf197a304267799708">https://gist.github.com/bgub/1da2c0064d53decf197a304267799708</a></p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>ml/ai</category>
            <category>open-source</category>
        </item>
        <item>
            <title><![CDATA[Introducing gom: GPU Monitoring across Containers]]></title>
            <link>https://www.bengubler.com/posts/2023-10-16-gom-gpu-monitor-nvidia-smi-replacement?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2023-10-16-gom-gpu-monitor-nvidia-smi-replacement</guid>
            <pubDate>Mon, 16 Oct 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[I published `gom`, a CLI tool for monitoring GPU usage across Docker containers.]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="/blog-images/gom-watch.png"/><link rel="preload" as="image" href="/blog-images/nvidia-smi.png"/><h2 id="tldr"><a aria-hidden="true" tabindex="-1" href="#tldr"><span class="icon icon-link"></span></a>TL;DR</h2>
<p><code>gom</code> stands for GPU Output Monitor. It&#x27;s a pip package that provides a CLI for monitoring GPU usage. Think of it as <code>nvidia-smi</code>, but faster and more minimalist. And it has a bonus feature: <strong>in environments where Docker containers are using GPUs, it will break down usage by container</strong>! (Don&#x27;t worry, it also works in environments without Docker and even inside Docker containers.)</p>
<p><em>I owe my colleague <a href="https://howe.vin/">Vin</a> credit for inspiring this project. He used GPT-4 to create an initial prototype in Bash, but I had to rewrite from scratch due to bugs and performance issues.</em></p>
<h2 id="instructions"><a aria-hidden="true" tabindex="-1" href="#instructions"><span class="icon icon-link"></span></a>Instructions</h2>
<ol>
<li>Run <code>pip3 install gom</code></li>
<li>Depending on your CUDA version, install the correct version of <code>pynvml</code></li>
<li>Run <code>gom show</code> (to show usage once) or <code>gom watch</code> (to monitor usage, updated roughly every second)</li>
</ol>
<h2 id="comparing-gom-and-nvidia-smi"><a aria-hidden="true" tabindex="-1" href="#comparing-gom-and-nvidia-smi"><span class="icon icon-link"></span></a>Comparing <code>gom</code> and <code>nvidia-smi</code></h2>
<p>I think the results speak for themselves :). This first screenshot is the result of running <code>gom watch</code>. You can see that four different Docker containers, <code>r0</code>, <code>r1</code>, <code>r2</code>, and <code>r3</code>, are each using a GPU quite heavily. There&#x27;s also slight usage of all GPUs that&#x27;s not coming from any container.</p>
<p><img src="/blog-images/gom-watch.png" alt="output of running gom watch command"/></p>
<p>This second screenshot is the result of running <code>nvidia-smi</code>. It&#x27;s complex and unnecessarily verbose. In more space than <code>gom</code>, it only manages to show information for 8 GPUs!</p>
<p><img src="/blog-images/nvidia-smi.png" alt="output of running nvidia-smi command"/></p>
<h2 id="conclusion"><a aria-hidden="true" tabindex="-1" href="#conclusion"><span class="icon icon-link"></span></a>Conclusion</h2>
<p>I created <code>gom</code> because I wanted to monitor GPU usage across different Docker containers. I use it frequently when doing ML tasks because it&#x27;s fast and the output fits on a small terminal. Hopefully it&#x27;s helpful for you. If you have suggestions, feel free to open an issue at the <a href="https://github.com/bgub/gom">GitHub repo</a>.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>ml/ai</category>
            <category>open-source</category>
        </item>
        <item>
            <title><![CDATA[Enroot on Slurm for Distributed ML: Part 2]]></title>
            <link>https://www.bengubler.com/posts/2023-09-11-enroot-on-slurm-for-distributed-ml-part-2?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2023-09-11-enroot-on-slurm-for-distributed-ml-part-2</guid>
            <pubDate>Mon, 11 Sep 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[How to use Enroot on Slurm for containerized multi-node training.]]></description>
            <content:encoded><![CDATA[<p><em>UPDATE 2024: I no longer recommend this method and have experienced several issues with it. Instead, I recommend using <a href="https://github.com/NVIDIA/pyxis">Pyxis</a>, a tool developed by NVIDIA that simplifies the process of running containers on HPC systems</em>._</p>
<p><em>This is part 2 of a 2-part series. <a href="./enroot-on-slurm-for-distributed-ml-part-1">Part 1</a> is available here.</em></p>
<p>In <a href="./enroot-on-slurm-for-distributed-ml-part-1">part 1</a>, we covered how to use Enroot on Slurm for containerized <em>single-node</em> training using <code>salloc</code>. In this post, we&#x27;ll cover how to use Enroot on Slurm for containerized <em>multi-node</em> training, and transition to using <code>sbatch</code>.</p>
<h2 id="step-1-slurm-launch-script"><a aria-hidden="true" tabindex="-1" href="#step-1-slurm-launch-script"><span class="icon icon-link"></span></a>Step 1: Slurm Launch Script</h2>
<p>We&#x27;ll end up creating several Bash files, all of which should be in the same directory as your training script. The first will be a Slurm launch file that we&#x27;ll run with <code>sbatch</code>. This file will contain the same commands we ran with <code>salloc</code> in <a href="../enroot-on-slurm-for-distributed-ml-part-1">part 1</a>, but declared using <code>#SBATCH</code> processing directives.</p>
<p><code>launch.sh</code></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="#!/bin/bash
#SBATCH -J &quot;JOBNAME&quot;
#SBATCH --nodes=2
#SBATCH --gpus-per-node=8
#SBATCH --cpus-per-task=128
#SBATCH --mem=2000G
#SBATCH --time=72:00:00
#SBATCH --qos=&lt;qos&gt;

export CUR_DIR=$(pwd)
srun --nodes=2 stage1.sh
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#!/bin/bash</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH -J &quot;JOBNAME&quot;</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH --nodes=2</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH --gpus-per-node=8</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH --cpus-per-task=128</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH --mem=2000G</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH --time=72:00:00</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH --qos=&lt;qos&gt;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> CUR_DIR</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">$(</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">pwd</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">srun</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --nodes=2</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> stage1.sh</span></span></code></pre></figure>
<p>Note that we create a variable <code>CUR_DIR</code> to store the current working directory (the directory where the <code>sbatch</code> command was run). I use this variable to share the location of my training directory between scripts, so I don&#x27;t have to hard-code paths. But it&#x27;s not required.</p>
<p>Slurm will automatically pass local environment variables through to the <code>srun</code> command, which will run the <code>stage1.sh</code> script on each node.</p>
<h2 id="step-2-enroot-launch-script"><a aria-hidden="true" tabindex="-1" href="#step-2-enroot-launch-script"><span class="icon icon-link"></span></a>Step 2. Enroot Launch Script</h2>
<p>Next, we&#x27;ll create a script that will be run on each node. This script will be responsible for launching the container and running the training script. We&#x27;ll call this script <code>stage1.sh</code>.</p>
<p><code>stage1.sh</code></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="#!/bin/bash

module load jq zstd pigz parallel libnvidia-container enroot

export MASTER_ADDR=$(scontrol show hostnames $SLURM_JOB_NODELIST | head -n 1) # get the IP address of the first node in the list
export MASTER_PORT=6000 # set the port to use for communication between nodes

enroot create --name image-name /path/to/image-name.sqsh

enroot start --env SLURM_NODEID \
             --env MASTER_ADDR \
             --env MASTER_PORT \
             --env SLURM_JOB_NAME \
             --env CUR_DIR \
             --mount /local/file/path:/image/file/path \
             --rw image-name \
             bash ${CUR_DIR}/stage2.sh
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#!/bin/bash</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">module</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> load</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> jq</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> zstd</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> pigz</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> parallel</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> libnvidia-container</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> enroot</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> MASTER_ADDR</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">$(</span><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">scontrol</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> show</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> hostnames</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> $SLURM_JOB_NODELIST </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">|</span><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1"> head</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> -n</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">) </span><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># get the IP address of the first node in the list</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> MASTER_PORT</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">6000</span><span style="--shiki-dark:#768390;--shiki-light:#6A737D"> # set the port to use for communication between nodes</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">enroot</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> create</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --name</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> image-name</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> /path/to/image-name.sqsh</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">enroot</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> start</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --env</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> SLURM_NODEID</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">             --env</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> MASTER_ADDR</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">             --env</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> MASTER_PORT</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">             --env</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> SLURM_JOB_NAME</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">             --env</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> CUR_DIR</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">             --mount</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> /local/file/path:/image/file/path</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">             --rw</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> image-name</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">             bash</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> ${CUR_DIR}</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">/stage2.sh</span></span></code></pre></figure>
<p>Note that we pass several important environment variables provided by Slurm, along with <code>CUR_DIR</code>, into the container. The <code>MASTER_ADDR</code> and <code>MASTER_PORT</code> variables are used by PyTorch&#x27;s distributed training backend to coordinate communication between nodes.</p>
<p>We also mount a local file path into the container (make sure it contains your training script!).</p>
<h2 id="step-3-training-script"><a aria-hidden="true" tabindex="-1" href="#step-3-training-script"><span class="icon icon-link"></span></a>Step 3. TrAIning Script</h2>
<p>Finally, we&#x27;ll create a training script that will be run inside the container. We&#x27;ll call this script <code>stage2.sh</code>.</p>
<p><code>stage2.sh</code></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="#!/bin/bash

export NCCL_DEBUG=INFO # if you want to see NCCL logs
export NODE_RANK=$SLURM_NODEID # set the node rank to the node ID (0, 1, 2, etc.)
echo NODE_RANK: $NODE_RANK # print the node rank for debugging purposes

# Run training script
# NOTE: modify as desired if you&#x27;re not using accelerate

accelerate launch --config_file ./accelerate_config.yaml --main_process_ip=$MASTER_ADDR --main_process_port=$MASTER_PORT --machine_rank $NODE_RANK ${CUR_DIR}/loop.py
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#!/bin/bash</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> NCCL_DEBUG</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">INFO </span><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># if you want to see NCCL logs</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> NODE_RANK</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">$SLURM_NODEID </span><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># set the node rank to the node ID (0, 1, 2, etc.)</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">echo</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> NODE_RANK:</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> $NODE_RANK </span><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># print the node rank for debugging purposes</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Run training script</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># NOTE: modify as desired if you&#x27;re not using accelerate</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">accelerate</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> launch</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --config_file</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> ./accelerate_config.yaml</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --main_process_ip=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">$MASTER_ADDR</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --main_process_port=</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">$MASTER_PORT</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --machine_rank</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> $NODE_RANK ${CUR_DIR}</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">/loop.py</span></span></code></pre></figure>
<p>Here I&#x27;ve used <a href="https://huggingface.co/docs/accelerate">accelerate</a> as a launcher for my distributed training script, but you can use whatever launcher you want. Just make sure you pass relevant environment variables through!</p>
<p>For the sake of completeness, here&#x27;s my <code>accelerate_config.yaml</code> file. It utilizes FSDP (Fully Sharded Data Parallel) to split model parameters and gradients across processes. This is a great way to train large models that won&#x27;t fit on just one GPU.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="yaml" data-theme="github-dark-dimmed github-light" __raw_string__="compute_environment: LOCAL_MACHINE
deepspeed_config: {}
distributed_type: FSDP
downcast_bf16: &quot;no&quot;
fsdp_config:
  fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP
  fsdp_backward_prefetch_policy: BACKWARD_PRE
  fsdp_offload_params: false
  fsdp_sharding_strategy: 1
  fsdp_state_dict_type: FULL_STATE_DICT
  fsdp_transformer_layer_cls_to_wrap: LlamaDecoderLayer
main_training_function: main
mixed_precision: &quot;no&quot;
num_machines: 2
num_processes: 16 # 8 GPUs per node * 2 nodes = 16 processes
use_cpu: false
"><code data-language="yaml" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">compute_environment</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">LOCAL_MACHINE</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">deepspeed_config</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: {}</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">distributed_type</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">FSDP</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">downcast_bf16</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;no&quot;</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">fsdp_config</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">  fsdp_auto_wrap_policy</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">TRANSFORMER_BASED_WRAP</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">  fsdp_backward_prefetch_policy</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">BACKWARD_PRE</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">  fsdp_offload_params</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">false</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">  fsdp_sharding_strategy</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">1</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">  fsdp_state_dict_type</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">FULL_STATE_DICT</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">  fsdp_transformer_layer_cls_to_wrap</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">LlamaDecoderLayer</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">main_training_function</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">main</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">mixed_precision</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;no&quot;</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">num_machines</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">2</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">num_processes</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">16</span><span style="--shiki-dark:#768390;--shiki-light:#6A737D"> # 8 GPUs per node * 2 nodes = 16 processes</span></span>
<span data-line=""><span style="--shiki-dark:#8DDB8C;--shiki-light:#22863A">use_cpu</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">false</span></span></code></pre></figure>
<h2 id="step-4-submit-the-job"><a aria-hidden="true" tabindex="-1" href="#step-4-submit-the-job"><span class="icon icon-link"></span></a>Step 4. Submit the Job</h2>
<p>Now that we&#x27;ve created all the necessary scripts, we can submit the job to Slurm using <code>sbatch</code>! From the directory containing the scripts, run:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="sbatch launch.sh
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">sbatch</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> launch.sh</span></span></code></pre></figure>
<p>Your job will be submitted to Slurm and run as soon as resources are available. Output logs will be stored at <code>slurm-&lt;jobid&gt;.out</code> in the current directory.</p>
<h2 id="conclusion"><a aria-hidden="true" tabindex="-1" href="#conclusion"><span class="icon icon-link"></span></a>Conclusion</h2>
<p>I hope this was helpful! There are many parts involved in getting distributed training working, but it&#x27;s not too difficult once you get over the initial learning curve.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>ml/ai</category>
        </item>
        <item>
            <title><![CDATA[Enroot on Slurm for Distributed ML: Part 1]]></title>
            <link>https://www.bengubler.com/posts/2023-09-08-enroot-on-slurm-for-distributed-ml-part-1?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2023-09-08-enroot-on-slurm-for-distributed-ml-part-1</guid>
            <pubDate>Fri, 08 Sep 2023 22:00:00 GMT</pubDate>
            <description><![CDATA[How to use Enroot on Slurm for containerized multi-node training.]]></description>
            <content:encoded><![CDATA[<p><em>This is part 1 of a 2-part series. <a href="./enroot-on-slurm-for-distributed-ml-part-2">Part 2</a> is available here.</em></p>
<p>In the lab where I work, we have access to a High Performance Computing (HPC) environment that uses the <a href="https://slurm.schedmd.com/documentation.html">Slurm Workload Manager</a>. Our HPC runs RHEL (Red Hat Enterprise Linux) 7, and individual users have significantly restricted permissions. We don&#x27;t have <code>sudo</code> access and can&#x27;t access the internet from the compute nodes.</p>
<p>In fact, the process of loading packages and updating drivers from inside a compute node is so difficult that it makes distributed training using modern software incredibly complicated. Luckily, there&#x27;s a solution: containerization.</p>
<p>We&#x27;ll use Docker to build an image on our local machine that contains all of the packages we need. Then we can transfer that image to the HPC, and use it to run our training script.</p>
<h2 id="step-1-build-a-docker-image-locally"><a aria-hidden="true" tabindex="-1" href="#step-1-build-a-docker-image-locally"><span class="icon icon-link"></span></a>Step 1: Build a Docker Image Locally</h2>
<p>I already wrote about <a href="./ultimate-ml-dockerfile">the Docker setup I use for machine learning</a>, so I won&#x27;t repeat myself here. The important thing is that you have a tagged Docker image with the packages you need to run your training script. Mine uses Ubuntu 20.04 and CUDA 11.8.</p>
<h2 id="step-2-squash-and-transfer-the-image"><a aria-hidden="true" tabindex="-1" href="#step-2-squash-and-transfer-the-image"><span class="icon icon-link"></span></a>Step 2: Squash and Transfer the Image</h2>
<p>Install <a href="https://github.com/NVIDIA/enroot">Enroot</a>, then run the following command to turn your Docker image into a squashfs file:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="enroot import dockerd://&lt;image-name&gt;
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">enroot</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> import</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> dockerd://</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">&lt;</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">image-nam</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">e</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">&gt;</span></span></code></pre></figure>
<p>This will create a file called <code>&lt;image-name&gt;.sqsh</code> in your current directory. Transfer this file to the HPC using <code>scp</code>.</p>
<h2 id="step-3-load-the-image-on-the-hpc"><a aria-hidden="true" tabindex="-1" href="#step-3-load-the-image-on-the-hpc"><span class="icon icon-link"></span></a>Step 3: Load the Image on the HPC</h2>
<p>Enter a compute node using <code>salloc --nodes=1 --gpus=8 --qos=&lt;qos&gt; --mem=2000G --time=72:00:00 --ntasks=1 --cpus-per-task=128</code>.</p>
<p>First we need to load the Enroot module:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="module load jq zstd pigz parallel libnvidia-container enroot
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">module</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> load</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> jq</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> zstd</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> pigz</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> parallel</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> libnvidia-container</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> enroot</span></span></code></pre></figure>
<p>On the HPC, create the image using the following command:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="enroot create --name image-name /path/to/image-name.sqsh
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">enroot</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> create</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --name</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> image-name</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> /path/to/image-name.sqsh</span></span></code></pre></figure>
<p>Then run it:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="enroot start --mount /local/file/path:/image/file/path \
             --rw image-name bash
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">enroot</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> start</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --mount</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> /local/file/path:/image/file/path</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">             --rw</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> image-name</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> bash</span></span></code></pre></figure>
<p>This will open up an interactive shell inside the container. Don&#x27;t forget the <code>--rw</code> flag, which makes the root filesystem writable. You can add as many <code>--mount</code> flags as you need to mount files and directories from the host machine.</p>
<p>If you want to pass through environment variables, you can use the <code>--env</code> flag along with the name of the environment variable on the host machine. For example, <code>--env SLURM_NODEID</code> will pass through the <code>SLURM_NODEID</code> environment variable.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>ml/ai</category>
        </item>
        <item>
            <title><![CDATA[Quick & Helpful Slurm Commands]]></title>
            <link>https://www.bengubler.com/posts/2023-09-08-quick-helpful-slurm-commands?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2023-09-08-quick-helpful-slurm-commands</guid>
            <pubDate>Fri, 08 Sep 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[A quick guide to using Slurm for distributed machine learning.]]></description>
            <content:encoded><![CDATA[<p>In the lab I work in, we have access to a High Performance Computing (HPC) environment that uses the <a href="https://slurm.schedmd.com/documentation.html">Slurm Workload Manager</a>.</p>
<p>I&#x27;ve been using it for a while now, and I&#x27;ve found a few commands that I use all the time. I thought I&#x27;d share them here in case they&#x27;re useful to anyone else.</p>
<h2 id="checking-job-status"><a aria-hidden="true" tabindex="-1" href="#checking-job-status"><span class="icon icon-link"></span></a>Checking Job Status</h2>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="# View all jobs
squeue
# Check the status of just your jobs
squeue -u &lt;username&gt;
# Check the status of jobs with a specific QOS
squeue -q &lt;QOS&gt;
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># View all jobs</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">squeue</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Check the status of just your jobs</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">squeue</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> -u</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> &lt;</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">usernam</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">e</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Check the status of jobs with a specific QOS</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">squeue</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> -q</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> &lt;</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">QO</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">S</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">&gt;</span></span></code></pre></figure>
<h2 id="cancelling-jobs"><a aria-hidden="true" tabindex="-1" href="#cancelling-jobs"><span class="icon icon-link"></span></a>Cancelling Jobs</h2>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="# Cancel a specific job
scancel &lt;job_id&gt;
# Cancel all your jobs
scancel -u &lt;username&gt;
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Cancel a specific job</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">scancel</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> &lt;</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">job_i</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">d</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Cancel all your jobs</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">scancel</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> -u</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> &lt;</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">usernam</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">e</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">&gt;</span></span></code></pre></figure>
<h2 id="requesting-a-node-interactively"><a aria-hidden="true" tabindex="-1" href="#requesting-a-node-interactively"><span class="icon icon-link"></span></a>Requesting a Node Interactively</h2>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="# Requesting a single node (this will open it up interactively in your terminal)
# When you exit the terminal, the node will be reallocated
salloc --nodes=1 --gpus=8 --qos=&lt;QOS&gt; --mem=2000G --time=72:00:00 --ntasks=1 --cpus-per-task=128
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Requesting a single node (this will open it up interactively in your terminal)</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># When you exit the terminal, the node will be reallocated</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">salloc</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --nodes=1</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --gpus=8</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --qos=</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">&lt;</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">QOS</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">&gt;</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --mem=2000G</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --time=72:00:00</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --ntasks=1</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --cpus-per-task=128</span></span></code></pre></figure>
<h2 id="submitting-a-job"><a aria-hidden="true" tabindex="-1" href="#submitting-a-job"><span class="icon icon-link"></span></a>Submitting a Job</h2>
<p>What if all of your compute nodes are allocated, or you don&#x27;t want your job to exit as soon as your terminal connection is closed? In that case, you can use <code>sbatch</code> to submit a job to the queue. It will automatically run as soon as it can allocate the resources.</p>
<p>This will take slightly more setup. Assume that the job we actually want to run is contained in <code>myjob.sh</code>. In order to submit that script as a job, we&#x27;ll first create a Bash script that will be run by Slurm. Let&#x27;s call it <code>run.sh</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="#!/bin/bash
#SBATCH -J &quot;JOBNAME&quot;
#SBATCH --nodes=1
#SBATCH --gpus-per-node=8
#SBATCH --cpus-per-task=128
#SBATCH --mem=2000G
#SBATCH --time=72:00:00
#SBATCH --qos=&lt;QOS&gt;

srun --nodes=1 myjob.sh
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#!/bin/bash</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH -J &quot;JOBNAME&quot;</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH --nodes=1</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH --gpus-per-node=8</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH --cpus-per-task=128</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH --mem=2000G</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH --time=72:00:00</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#SBATCH --qos=&lt;QOS&gt;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">srun</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --nodes=1</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> myjob.sh</span></span></code></pre></figure>
<p>Note that we&#x27;re using the <code>#SBATCH</code> processing directive to pass in the parameters that we would have passed to <code>salloc</code> before. We&#x27;re also using <code>srun</code> to run our actual job; it will handle running the script across multiple nodes, if we so desire.</p>
<p>Finally, to launch our script, we&#x27;ll run:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="sbatch run.sh
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">sbatch</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> run.sh</span></span></code></pre></figure>
<h2 id="conclusion"><a aria-hidden="true" tabindex="-1" href="#conclusion"><span class="icon icon-link"></span></a>Conclusion</h2>
<p>That&#x27;s it! I hope this was helpful. If you have any questions, you can ask ChatGPT or Bard (they&#x27;ll give either incredibly helpful or completely incorrect answers, but it&#x27;s worth a shot!)</p>
<p>You can also look through the <a href="https://slurm.schedmd.com/documentation.html">Slurm documentation</a> or the <a href="https://leo.leung.xyz/wiki/Slurm">Leo&#x27;s notes</a> page on Slurm for more information.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>ml/ai</category>
        </item>
        <item>
            <title><![CDATA[Setting Up Docker for Machine Learning]]></title>
            <link>https://www.bengubler.com/posts/2023-09-08-ultimate-ml-dockerfile?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2023-09-08-ultimate-ml-dockerfile</guid>
            <pubDate>Fri, 08 Sep 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[The Dockerfile I use to set up my machine learning environment.]]></description>
            <content:encoded><![CDATA[<p><em>UPDATE 2024: I&#x27;ve updated this post to be based on the <code>nvcr.io/nvidia/pytorch</code> image, which I always use these days because of its great NVIDIA + NCCL + Infiniband support. I also simplified the file and modified it to use <code>gom</code> for GPU monitoring.</em></p>
<p>This post is mainly meant for coworkers, but it might be useful for others as well. I&#x27;ll be sharing the Dockerfile that I use to set up my machine learning environment. It&#x27;s based on NVIDIA&#x27;s <code>pytorch</code> image, but I&#x27;ve added a few things (upgraded Pip packages, GitHub CLI, Starship prompt, <a href="./2023-10-16-gom-gpu-monitor-nvidia-smi-replacement">GPU monitoring</a>, etc.) that I find useful.</p>
<h2 id="setup"><a aria-hidden="true" tabindex="-1" href="#setup"><span class="icon icon-link"></span></a>Setup</h2>
<p>Copy and paste the below <code>Dockerfile</code> to a new directory. Feel free to add or remove anything you want — I&#x27;ll likely update this post as I make changes to my setup.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="dockerfile" data-theme="github-dark-dimmed github-light" __raw_string__="# Base image with Ubuntu 22.04, Python 3.10, CUDA 12.4
FROM nvcr.io/nvidia/pytorch:24.04-py3

#####################
# PYTHON PACKAGES   #
#####################

# Disable the &quot;running pip as the &#x27;root&#x27; user can...&quot; warning
ENV PIP_ROOT_USER_ACTION=ignore

# Upgrade pip
RUN pip3 install --upgrade pip

# Upgrade &amp; install useful machine learning packages
RUN pip3 install --upgrade transformers accelerate deepspeed fire tqdm openai numpy rouge_score wandb ipython emoji tokenizers evaluate matplotlib seaborn lm-eval jupyter nltk tiktoken aiolimiter swifter pytorch-lightning lightning sentencepiece jsonargparse[signatures] bitsandbytes datasets zstandard rich transformer_lens librosa soundfile gom git+https://github.com/stanfordnlp/pyvene.git git+https://github.com/stanfordnlp/pyreft.git

# Install TorchAudio nightly
RUN pip3 install --no-deps torchaudio

# Fix incorrect Docker pip package, so gom works
RUN mv /usr/local/lib/python3.10/dist-packages/docker /usr/local/lib/python3.10/dist-packages/docker_old

#####################
# GH CLI &amp; STARSHIP #
#####################

# GitHub CLI
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
    &amp;&amp; chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
    &amp;&amp; echo &quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main&quot; | tee /etc/apt/sources.list.d/github-cli.list &gt; /dev/null \
    &amp;&amp; apt-get update \
    &amp;&amp; apt-get install gh -y

RUN apt-get upgrade -y

# Starship Prompt
RUN curl -sS https://starship.rs/install.sh -o starship-install.sh 
RUN sh -posix starship-install.sh --yes
RUN echo &#x27;eval &quot;$(starship init bash)&quot;&#x27; &gt;&gt; ~/.bashrc

# Starship Config
RUN echo $&#x27;[character]\n\
    success_symbol = &quot;[λ](bold green) &quot;\n\
    error_symbol = &quot;[λ](bold red) &quot;\n\
    \n\
    [aws]\n\
    disabled = true&#x27; &gt; /root/.config/starship.toml
"><code data-language="dockerfile" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Base image with Ubuntu 22.04, Python 3.10, CUDA 12.4</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">FROM</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> nvcr.io/nvidia/pytorch:24.04-py3</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#####################</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># PYTHON PACKAGES   #</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#####################</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Disable the &quot;running pip as the &#x27;root&#x27; user can...&quot; warning</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">ENV</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> PIP_ROOT_USER_ACTION=ignore</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Upgrade pip</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> pip3 install --upgrade pip</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Upgrade &amp; install useful machine learning packages</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> pip3 install --upgrade transformers accelerate deepspeed fire tqdm openai numpy rouge_score wandb ipython emoji tokenizers evaluate matplotlib seaborn lm-eval jupyter nltk tiktoken aiolimiter swifter pytorch-lightning lightning sentencepiece jsonargparse[signatures] bitsandbytes datasets zstandard rich transformer_lens librosa soundfile gom git+https://github.com/stanfordnlp/pyvene.git git+https://github.com/stanfordnlp/pyreft.git</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Install TorchAudio nightly</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> pip3 install --no-deps torchaudio</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Fix incorrect Docker pip package, so gom works</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> mv /usr/local/lib/python3.10/dist-packages/docker /usr/local/lib/python3.10/dist-packages/docker_old</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#####################</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># GH CLI &amp; STARSHIP #</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">#####################</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># GitHub CLI</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    &amp;&amp; chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    &amp;&amp; echo </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> | tee /etc/apt/sources.list.d/github-cli.list &gt; /dev/null \</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    &amp;&amp; apt-get update \</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    &amp;&amp; apt-get install gh -y</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> apt-get upgrade -y</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Starship Prompt</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> curl -sS https://starship.rs/install.sh -o starship-install.sh </span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> sh -posix starship-install.sh --yes</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> echo </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&#x27;eval &quot;$(starship init bash)&quot;&#x27;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> &gt;&gt; ~/.bashrc</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D"># Starship Config</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> echo $</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&#x27;[character]</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5">\n</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">\</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">    success_symbol = &quot;[λ](bold green) &quot;</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5">\n</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">\</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">    error_symbol = &quot;[λ](bold red) &quot;</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5">\n</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">\</span></span>
<span data-line=""><span style="--shiki-dark:#F47067;--shiki-light:#005CC5">    \n</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">\</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">    [aws]</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5">\n</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">\</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">    disabled = true&#x27;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> &gt; /root/.config/starship.toml</span></span></code></pre></figure>
<h2 id="building-the-image"><a aria-hidden="true" tabindex="-1" href="#building-the-image"><span class="icon icon-link"></span></a>Building the Image</h2>
<p>After you&#x27;ve created the files, you can build the image with:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="cd /path/to/directory
docker build -t YOUR_IMAGE_NAME_HERE .
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">cd</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> /path/to/directory</span></span>
<span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">docker</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> build</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> -t</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> YOUR_IMAGE_NAME_HERE</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> .</span></span></code></pre></figure>
<h2 id="running-the-image"><a aria-hidden="true" tabindex="-1" href="#running-the-image"><span class="icon icon-link"></span></a>Running the Image</h2>
<p>You can run the image with:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark-dimmed github-light" __raw_string__="docker run -d --rm -it \
    --gpus all \
    --name YOUR_CONTAINER_NAME \
    --mount type=bind,source=YOUR_HOME_DIR,target=YOUR_HOME_DIR \
    -w YOUR_HOME_DIR \
    YOUR_IMAGE_NAME_HERE:latest
"><code data-language="bash" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1">docker</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> run</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> -d</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> --rm</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5"> -it</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">    --gpus</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> all</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">    --name</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> YOUR_CONTAINER_NAME</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">    --mount</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> type=bind,source=YOUR_HOME_DIR,target=YOUR_HOME_DIR</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">    -w</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62"> YOUR_HOME_DIR</span><span style="--shiki-dark:#F47067;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">    YOUR_IMAGE_NAME_HERE:latest</span></span></code></pre></figure>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>ml/ai</category>
        </item>
        <item>
            <title><![CDATA[Accelerate vs. DeepSpeed vs. FSDP]]></title>
            <link>https://www.bengubler.com/posts/2023-08-29-accelerate-deepspeed-fsdp?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2023-08-29-accelerate-deepspeed-fsdp</guid>
            <pubDate>Tue, 29 Aug 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Which one should you use for distributed training?]]></description>
            <content:encoded><![CDATA[<h2 id="introduction"><a aria-hidden="true" tabindex="-1" href="#introduction"><span class="icon icon-link"></span></a>Introduction</h2>
<p>There are many different libraries and strategies for distributed training. In this article, we&#x27;ll look at three of the most popular: <a href="https://huggingface.co/docs/accelerate/index">Accelerate</a>, <a href="https://www.deepspeed.ai/">DeepSpeed</a>, and <a href="https://engineering.fb.com/2021/07/15/open-source/fsdp/">FSDP</a>. We&#x27;ll discuss the differences between them, and when you might want to use one over the other.</p>
<h2 id="accelerate"><a aria-hidden="true" tabindex="-1" href="#accelerate"><span class="icon icon-link"></span></a>Accelerate</h2>
<p><a href="https://huggingface.co/docs/accelerate/index">Accelerate</a> is a popular library developed and maintained by HuggingFace. You can think of it as a wrapper around <code>torch.distributed</code>. Essentially, it allows you to simply run training or <a href="./multi-gpu-inference-with-accelerate">inference</a> across multiple GPUs or nodes.</p>
<p>In its most basic form, you use Accelerate to initialize a PyTorch model on each GPU. By simply making a few modifications to your training loop, Accelerate will handle data parallelism for you.</p>
<p>If your model is too large to fit on any one GPU, you can use Accelerate to split the model across multiple GPUs by passing <code>device_map=&quot;auto&quot;</code> into the transformers <code>from_pretrained</code> method. Be warned — you can only use <code>device_map=&quot;auto&quot;</code> if you&#x27;re running with <code>num_processes=1</code>, because you&#x27;re only initializing one model.</p>
<p>If you need more sophisticated model sharding (&quot;sharding&quot; refers to splitting a model across devices) you can use DeepSpeed or FSDP alongside Accelerate</p>
<h2 id="deepspeed"><a aria-hidden="true" tabindex="-1" href="#deepspeed"><span class="icon icon-link"></span></a>DeepSpeed</h2>
<p><a href="https://www.deepspeed.ai/">DeepSpeed</a> offers the Zero Redundancy Optimizer (ZeRO). It&#x27;s called &quot;Zero Redundancy&quot; because it allows you to partition a model across multiple GPUs without having to replicate the model&#x27;s parameters across each GPU. This is a huge benefit, because it allows you to train models that are larger than the memory of any one GPU.</p>
<p>There are three stages of ZeRO:</p>
<ul>
<li><strong>ZeRO Stage 1</strong> partitions optimizer states</li>
<li><strong>ZeRO Stage 2</strong> also partitions gradients</li>
<li><strong>ZeRO Stage 3</strong> also partitions parameters</li>
</ul>
<p>If you&#x27;re still running into memory issues, DeepSpeed allows you to offload the optimizer state, gradients, and some model weights to CPU memory or NVMe storage. This is called &quot;<strong>ZeRO-Infinity</strong>,&quot; and — though significantly slower than training without offload — allows for training truly huge models.</p>
<h2 id="fsdp"><a aria-hidden="true" tabindex="-1" href="#fsdp"><span class="icon icon-link"></span></a>FSDP</h2>
<p><a href="https://engineering.fb.com/2021/07/15/open-source/fsdp/">FSDP</a> stands for &quot;Fully Sharded Data Parallel.&quot; It was originally developed by Facebook AI Research and released in the Fairscale library, but upstream support was <a href="https://pytorch.org/blog/introducing-pytorch-fully-sharded-data-parallel-api/">added natively to PyTorch</a> in PyTorch version 1.11.</p>
<p>It does essentially the same thing as DeepSpeed ZeRO — manage sharding of optimizer states, gradients, and model parameters. It also supports CPU offload. One helpful feature is that it can serve as a drop-in replacement for DistributedDataParallel.</p>
<h2 id="summary"><a aria-hidden="true" tabindex="-1" href="#summary"><span class="icon icon-link"></span></a>Summary</h2>
<ul>
<li>Accelerate is a wrapper around <code>torch.distributed</code> that allows you to easily run training or inference across multiple GPUs or nodes. It can also be used for simple model partitioning, and works well with both DeepSpeed and FSDP for more advanced use cases.</li>
<li>DeepSpeed and FSDP are two different implementations of the same idea: sharding model parameters, gradients, and optimizer states across multiple GPUs. They both support CPU offload and can be used in conjunction with Accelerate.</li>
</ul>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>ml/ai</category>
        </item>
        <item>
            <title><![CDATA[LLMs Will Never Be Able to Do (Complicated) Math]]></title>
            <link>https://www.bengubler.com/posts/2023-08-23-llms-limitation-recursion?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2023-08-23-llms-limitation-recursion</guid>
            <pubDate>Wed, 23 Aug 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Since contemporary LLM architectures lack recursion, they're fundamentally incapable of doing some math operations.]]></description>
            <content:encoded><![CDATA[<p><em>UPDATE 2024: Just to clarify, this post is about mathematical operations that inherently involve multiple recursive steps, like exponentiation. As some <a href="https://arxiv.org/abs/2405.17399v1">cool research has shown</a>, Transformers can learn to do basic arithmetic rather well with some tweaks. Adding a &quot;scratchpad&quot; can further improve model performance and may be a good workaround to the problems mentioned in this article.</em></p>
<h2 id="the-problem"><a aria-hidden="true" tabindex="-1" href="#the-problem"><span class="icon icon-link"></span></a>The Problem</h2>
<p>LLMs have tremendous potential in many areas, but most contemporary models have one inherent limitation: they&#x27;re solely feed-forward in structure. This means that data flows linearly from input to output, with no recursion or backtracking. This enables incredibly fast and efficient training using gradient descent and back-propagation. Computations can be done in parallel using matrix multiplication.</p>
<p>Unfortunately, their lack of recursion makes some types of mathematical operations impossible. Consider exponentiation. ChatGPT can handle simple exponent problems, but when asked what X^Y is for high values of X or Y, it becomes inaccurate.</p>
<p>Though exponential operations can be broken down into a linear sequence, it&#x27;s impossible for a finite, feed-forward neural net to handle any possible recursive operation (i.e., X^Y with any possible value for Y). The amount of recursion an LLM can &quot;simulate&quot; is limited by the number of its parameters and layers.</p>
<h2 id="summary"><a aria-hidden="true" tabindex="-1" href="#summary"><span class="icon icon-link"></span></a>Summary</h2>
<p>Lack of recursion is an inherent design limitation in current GPT-style LLMs which prevents them from being able to perform complicated math operations. The fact is, though, that doesn&#x27;t matter in most use cases for LLMs! They&#x27;re still powerful and helpful in a wide variety of circumstances.</p>
<h2 id="fun-stuff"><a aria-hidden="true" tabindex="-1" href="#fun-stuff"><span class="icon icon-link"></span></a>Fun Stuff</h2>
<p>There&#x27;s still a lot of work to be done in understanding the behavior of trained large language models. Here&#x27;s something fascinating I found while writing this article:</p>
<p>When I asked ChatGPT what 7^15 equals, it gave the answer <strong>170,859,375</strong>. The correct answer is <strong>4,747,561,509,943</strong>.</p>
<p>Though the answer is obviously incorrect, <strong>170,859,375</strong> has a unique property: it factors into <strong>(3^7)*(5^7)</strong>. The model seems to have converted <strong>A^(B*C)</strong> into <strong>(B^A)*(C^A)</strong> under the hood. I&#x27;d be interested to learn why this happens!</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>ml/ai</category>
        </item>
        <item>
            <title><![CDATA[One Config File to Rule Them All]]></title>
            <link>https://www.bengubler.com/posts/2023-06-29-global-config-js?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2023-06-29-global-config-js</guid>
            <pubDate>Thu, 29 Jun 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[JavaScript tools have too many config files. Let's combine them.]]></description>
            <content:encoded><![CDATA[<p>Modern web development involves working with multiple JS build tools and frameworks, each requiring their own configuration files. Managing these configuration files, such as <code>.eslintrc</code>, <code>next.config.js</code>, and <code>tailwind.config.js</code>, can become cumbersome and time-consuming. In this blog post, I&#x27;ll explore the idea of combining these configuration files into a single file called <code>global.config.js</code>, centralizing project configuration and reducing distractions.</p>
<h2 id="config-files-everywhere"><a aria-hidden="true" tabindex="-1" href="#config-files-everywhere"><span class="icon icon-link"></span></a>Config Files Everywhere</h2>
<p>As I&#x27;m writing this blog post, my project root contains an <code>.eslintrc.json</code>, <code>next.config.js</code>, <code>postcss.config.js</code>, <code>tailwind.config.js</code>, and <code>tsconfig.json</code>. Although my configuration is pretty out-of-the-box and each file is less than 30 lines, those files take up valuable space in my VSCode sidebar and distract from what&#x27;s important: my source code.</p>
<p>My case is far from exceptional. Some other projects use far more configuration files. Imagine how cluttered a project can get when you add a <code>.babelrc</code>, <code>prettier.config.js</code>, <code>jest.config.js</code>, <code>cypress.json</code>, etc.</p>
<h2 id="the-solution-globalconfigjs"><a aria-hidden="true" tabindex="-1" href="#the-solution-globalconfigjs"><span class="icon icon-link"></span></a>The Solution: <code>global.config.js</code></h2>
<p>I&#x27;d like to propose a simple solution: consolidating configuration files in a file called <code>global.config.js</code>. The configuration for each tool would be stored in the exported object, under a key with the name of the npm package.</p>
<p>Projects should still allow usage of individual configuration files for cases when configuration is large and complex (e.g. <code>tsconfig.json</code>), but should first scan to see if a <code>global.config.js</code> exists and configuration for their tool is present.</p>
<p>Here&#x27;s what a simple <code>global.config.js</code> might look like:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark-dimmed github-light" __raw_string__="module.exports = {
  eslint: {
    extends: [&quot;next/core-web-vitals&quot;]
  },
  postcss: {
    plugins: {
      tailwindcss: {},
      autoprefixer: {}
    }
  },
  tailwindcss: {
    content: [&quot;./src/pages/**/*.{js,ts,jsx,tsx,mdx}&quot;],
    theme: {
      extend: {
        backgroundImage: {
          &quot;gradient-radial&quot;: &quot;radial-gradient(var(--tw-gradient-stops))&quot;
        }
      }
    },
    plugins: [require(&quot;@tailwindcss/typography&quot;)]
  }
}
"><code data-language="js" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">module</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">.</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">exports</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  eslint: {</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    extends: [</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;next/core-web-vitals&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  postcss: {</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    plugins: {</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">      tailwindcss: {},</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">      autoprefixer: {}</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  tailwindcss: {</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    content: [</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;./src/pages/**/*.{js,ts,jsx,tsx,mdx}&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    theme: {</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">      extend: {</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">        backgroundImage: {</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">          &quot;gradient-radial&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;radial-gradient(var(--tw-gradient-stops))&quot;</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">      }</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    plugins: [</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">require</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;@tailwindcss/typography&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)]</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="but-what-about-types"><a aria-hidden="true" tabindex="-1" href="#but-what-about-types"><span class="icon icon-link"></span></a>But What about Types?</h2>
<p>Type completion can be easily enabled for <code>global.config.js</code> by adding type definitions in comments, like Tailwind and NextJS already do in their config files.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark-dimmed github-light" __raw_string__="module.exports = {
  // ...

  /** @type {import(&#x27;tailwindcss&#x27;).Config} */
  tailwindcss: {
    content: [&quot;./src/pages/**/*.{js,ts,jsx,tsx,mdx}&quot;],
    theme: {
      extend: {
        backgroundImage: {
          &quot;gradient-radial&quot;: &quot;radial-gradient(var(--tw-gradient-stops))&quot;
        }
      }
    },
    plugins: [require(&quot;@tailwindcss/typography&quot;)]
  }
}
"><code data-language="js" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">module</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">.</span><span style="--shiki-dark:#6CB6FF;--shiki-light:#005CC5">exports</span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">  // ...</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#768390;--shiki-light:#6A737D">  /** </span><span style="--shiki-dark:#F47067;--shiki-light:#D73A49">@type</span><span style="--shiki-dark:#F69D50;--shiki-light:#6F42C1"> {import(&#x27;tailwindcss&#x27;).Config}</span><span style="--shiki-dark:#768390;--shiki-light:#6A737D"> */</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  tailwindcss: {</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    content: [</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;./src/pages/**/*.{js,ts,jsx,tsx,mdx}&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    theme: {</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">      extend: {</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">        backgroundImage: {</span></span>
<span data-line=""><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">          &quot;gradient-radial&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">: </span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;radial-gradient(var(--tw-gradient-stops))&quot;</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">      }</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">    plugins: [</span><span style="--shiki-dark:#DCBDFB;--shiki-light:#6F42C1">require</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">(</span><span style="--shiki-dark:#96D0FF;--shiki-light:#032F62">&quot;@tailwindcss/typography&quot;</span><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">)]</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#ADBAC7;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="next-steps"><a aria-hidden="true" tabindex="-1" href="#next-steps"><span class="icon icon-link"></span></a>Next Steps</h2>
<p>If you like this idea, submit a PR to your favorite build tool! If you don&#x27;t, let me know why on Twitter. Or don&#x27;t and just keep going about your life.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>open-source</category>
        </item>
        <item>
            <title><![CDATA[Introducing Eta v3]]></title>
            <link>https://www.bengubler.com/posts/2023-06-22-introducing-eta-v3?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2023-06-22-introducing-eta-v3</guid>
            <pubDate>Thu, 22 Jun 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[The next version of Eta, an embedded JS template engine, brings API and documentation improvements.]]></description>
            <content:encoded><![CDATA[<h2 id="background"><a aria-hidden="true" tabindex="-1" href="#background"><span class="icon icon-link"></span></a>Background</h2>
<p>Today, Eta is my most popular and widely used open-source project. But when I first published it 3 years ago, it wasn&#x27;t one of my primary focuses. In fact, I created Eta as a slimmed-down variation of <a href="https://squirrelly.js.org">Squirrelly</a>, a more complex template engine with features like helpers and filters.</p>
<p>As time passed, I realized that for most projects, an embedded template engine was actually a better fit than something more complex. Projects which needed complex or client-side HTML processing typically used a framework like React or Vue. Eta&#x27;s performance and low bundle size, meanwhile, made it a great fit for projects which needed fast processing, low memory usage, or to handle non-XML languages.</p>
<p>At the same time, Eta had become increasingly popular thanks to its speed, Deno support, and syntactic advantages over EJS. Given those factors, I decided to make Eta my main focus. I spent time writing tutorials, fixing issues, and polishing documentation.</p>
<p>After several years and some time spent away from programming as a missionary, I finally had time to work on Eta again. I decided to make some big changes to the project, including the build system, API, and documentation.</p>
<h2 id="build-system-updates"><a aria-hidden="true" tabindex="-1" href="#build-system-updates"><span class="icon icon-link"></span></a>Build System Updates</h2>
<p>Despite Eta&#x27;s advantages and features, it had some big problems. One such problem was the build system. Complex and unwieldy, it was difficult to maintain and update. I dealt with complex configuration files and the necessity of transpiling a version specifically for Deno.</p>
<p>Changes in version 3:</p>
<ul>
<li>Using <a href="https://github.com/developit/microbundle">microbundle</a> to bundle the library helped me avoid the need for complex configuration files.</li>
<li>Using GitHub Actions to run tests and collect coverage allowed me to consolidate the services I used.</li>
<li>By setting <code>allowImportingTsExtensions: true</code> in <code>tsconfig.json</code>, I was able to avoid using <a href="https://github.com/garronej/denoify">Denoify</a> for a separate Deno build.</li>
</ul>
<h2 id="api-changes"><a aria-hidden="true" tabindex="-1" href="#api-changes"><span class="icon icon-link"></span></a>API Changes</h2>
<p>Another problem was the API. Simple methods like <code>eta.render()</code> had many function overloads, making types difficult to infer and usage unintuitive. A custom configuration object could be passed in when calling user-exposed functions like <code>render</code>, <code>parse</code>, and <code>compile</code>. In practice, that meant the user-provided configuration had to be merged with the default configuration every time any of those functions was called.</p>
<p>Changes in version 3:</p>
<ul>
<li>There&#x27;s only one export, a named class called <code>Eta</code>. This class has a single constructor, which processes a configuration object and generates template caches at instantiation time.</li>
<li>The <code>render()</code> and <code>renderAsync()</code> functions now have a single function signature.<!-- -->
<ul>
<li>In Eta v2, <code>render()</code> and <code>renderAsync()</code> could be used to render either named templates or template strings. Eta v3 introduces two new functions to render template strings: <code>renderString()</code> and <code>renderStringAsync()</code>.</li>
</ul>
</li>
<li>The <code>readFile()</code> and <code>resolvePath()</code> functions, which Eta uses internally, can be overridden as class methods by the user.</li>
<li>Internal variables and methods inside each compiled template are stored in the <code>__eta</code> object, rather than across several variables including <code>__res</code>.</li>
<li>Rather than allowing users to specify one <code>root</code> directory and multiple <code>views</code> directories, users may just specify a single <code>views</code> directory. This directory is used as the root directory for all template resolution. All template files must be inside this directory or a subdirectory of it, improving template security and reducing expensive file-lookup operations.</li>
</ul>
<h2 id="developer-experience-changes"><a aria-hidden="true" tabindex="-1" href="#developer-experience-changes"><span class="icon icon-link"></span></a>Developer Experience Changes</h2>
<p>One of the biggest changes in Eta v3 was the addition of detailed runtime errors (inspired by EJS). Consider a template like the following, which will throw because of an undefined variable:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="eta" data-theme="github-dark-dimmed github-light" __raw_string__="Template header
&lt;%= undefinedVariable %&gt;
Lorem Ipsum
"><code data-language="eta" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span>Template header</span></span>
<span data-line=""><span>&lt;%= undefinedVariable %&gt;</span></span>
<span data-line=""><span>Lorem Ipsum</span></span></code></pre></figure>
<p>Eta v2 would throw an error with some generic info, but it wasn&#x27;t incredibly helpful. In contrast, Eta v3 throws a detailed error with the template name, line number, and error message:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="text" data-theme="github-dark-dimmed github-light" __raw_string__="EtaError [ReferenceError]: .../my-dir/templates/runtime-error.eta:2
    1| Template header
 &gt;&gt; 2| &lt;%= undefinedVariable %&gt;
    3| Lorem Ipsum

undefinedVariable is not defined
"><code data-language="text" data-theme="github-dark-dimmed github-light" style="display:grid"><span data-line=""><span>EtaError [ReferenceError]: .../my-dir/templates/runtime-error.eta:2</span></span>
<span data-line=""><span>    1| Template header</span></span>
<span data-line=""><span> &gt;&gt; 2| &lt;%= undefinedVariable %&gt;</span></span>
<span data-line=""><span>    3| Lorem Ipsum</span></span>
<span data-line=""> </span>
<span data-line=""><span>undefinedVariable is not defined</span></span></code></pre></figure>
<h2 id="documentation-changes"><a aria-hidden="true" tabindex="-1" href="#documentation-changes"><span class="icon icon-link"></span></a>Documentation Changes</h2>
<p>The documentation for Eta v2 was extensive but very difficult to navigate. Information about the project was split over 40+ (!) documentation pages, found in multiple folders spread across 3 different website sections.</p>
<p>The documentation for Eta v3 takes up 9 pages, all found in the same part of the website (<a href="https://eta.js.org">eta.js.org</a>). Topics like template syntax and API overview are covered in a single page, rather than being split across multiple pages.</p>
<h2 id="the-future-of-eta"><a aria-hidden="true" tabindex="-1" href="#the-future-of-eta"><span class="icon icon-link"></span></a>The Future of Eta</h2>
<p>I&#x27;m proud of the changes included in Eta v3, and of the project as a whole. Much thanks to those who contributed to the project through PRs, issues, and suggestions. Additional thanks to projects like <a href="https://github.com/mde/ejs">ejs</a>, from which Eta continues to draw inspiration.</p>
<p>I see Eta as mostly feature-complete at this point, though I&#x27;ll continue to fix bugs and add some small features. I&#x27;d encourage current users of the library to upgrade to v3, and I hope new users will find Eta to be a great fit for their projects.</p>
<h2 id="links"><a aria-hidden="true" tabindex="-1" href="#links"><span class="icon icon-link"></span></a>Links</h2>
<ul>
<li><a href="https://github.com/eta-dev/eta">Eta on GitHub</a></li>
<li><a href="https://www.npmjs.com/package/eta">Eta on npm</a></li>
<li><a href="https://eta.js.org">Eta website and docs</a></li>
</ul>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
            <category>open-source</category>
        </item>
        <item>
            <title><![CDATA[Introducing My New Website]]></title>
            <link>https://www.bengubler.com/posts/2023-05-06-my-new-website?utm_campaign=feed&amp;utm_source=rss</link>
            <guid isPermaLink="false">https://www.bengubler.com/posts/2023-05-06-my-new-website</guid>
            <pubDate>Sat, 06 May 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[I created a brand-new personal website using Next.js, Tailwind CSS, and MDX.]]></description>
            <content:encoded><![CDATA[<p>I&#x27;m excited to present the first version of my new personal website!</p>
<p>I built this website using <a href="https://nextjs.org/">Next.js 13</a>, with the new App Router architecture. Other tools I used include <a href="https://tailwindcss.com/">Tailwind CSS</a>, <a href="https://mdxjs.com/">MDX</a>, and <a href="https://github.com/code-hike/bright">Bright</a>. I&#x27;m hosting it on <a href="https://vercel.com/">Vercel</a>.</p>
<p>I hope to post more about the process of building this website soon. In the meantime, you can check out the <a href="https://github.com/bgub/bengubler.com">source code</a>.</p>
<p>I&#x27;m grateful for the official NextJS docs and <a href="https://nextjs.org/learn/basics/create-nextjs-app">tutorial</a>, which I used heavily; <a href="https://maxleiter.com/blog/build-a-blog-with-nextjs-13#sitemap-support-sitemapjs">this fantastic blog post by Max Leiter</a>, about using the new App Router architecture; and the <a href="https://precedent.dev/">Precedent theme</a>, from which I borrowed my header component.</p>]]></content:encoded>
            <author>hello@bengubler.com (Ben Gubler)</author>
        </item>
    </channel>
</rss>