<?xml version="1.0" encoding="UTF-8"?>
<!-- Template from here: https://github.com/diverso/jekyll-rss-feeds -->
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
		<title>Finbarr Timbers</title>
		<description>Personal website for Finbarr Timbers</description>
		<link>https://finbarr.ca</link>
		<atom:link href="https://finbarr.ca/feed.xml" rel="self" type="application/rss+xml" />
		
			<item>
				<title>Latency numbers everyone should know</title>
				<description>&lt;p&gt;There&apos;s a &lt;a href=&quot;https://static.googleusercontent.com/media/sre.google/en//static/pdf/rule-of-thumb-latency-numbers-letter.pdf&quot;&gt;Google PDF&lt;/a&gt; with a bunch of useful latency numbers that software engineers should know. I asked ChatGPT to make it into a markdown file for me.&lt;/p&gt;

&lt;h2 id=&quot;latency-table&quot;&gt;Latency Table&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Operation&lt;/th&gt;
      &lt;th&gt;Time (ns)&lt;/th&gt;
      &lt;th&gt;Time (ms)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;L1 cache reference&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Branch misprediction&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;L2 cache reference&lt;/td&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Mutex lock/unlock&lt;/td&gt;
      &lt;td&gt;17&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Main memory reference&lt;/td&gt;
      &lt;td&gt;100&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Compress 1 kB with Zippy&lt;/td&gt;
      &lt;td&gt;2,000&lt;/td&gt;
      &lt;td&gt;0.002&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Read 1 MB sequentially from memory&lt;/td&gt;
      &lt;td&gt;10,000&lt;/td&gt;
      &lt;td&gt;0.010&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Send 2 kB over 10 Gbps network&lt;/td&gt;
      &lt;td&gt;1,600&lt;/td&gt;
      &lt;td&gt;0.0016&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;SSD 4kB Random Read&lt;/td&gt;
      &lt;td&gt;20,000&lt;/td&gt;
      &lt;td&gt;0.020&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Read 1 MB sequentially from SSD&lt;/td&gt;
      &lt;td&gt;1,000,000&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Round trip within same datacenter&lt;/td&gt;
      &lt;td&gt;500,000&lt;/td&gt;
      &lt;td&gt;0.5&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Read 1 MB sequentially from disk&lt;/td&gt;
      &lt;td&gt;5,000,000&lt;/td&gt;
      &lt;td&gt;5&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Read 1 MB sequentially from 1 Gbps network&lt;/td&gt;
      &lt;td&gt;10,000,000&lt;/td&gt;
      &lt;td&gt;10&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Disk seek&lt;/td&gt;
      &lt;td&gt;10,000,000&lt;/td&gt;
      &lt;td&gt;10&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;TCP packet round trip between continents&lt;/td&gt;
      &lt;td&gt;150,000,000&lt;/td&gt;
      &lt;td&gt;150&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;derived-throughput-estimates&quot;&gt;Derived Throughput Estimates&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Sequential read from HDD: ~200 MB/s&lt;/li&gt;
  &lt;li&gt;Sequential read from SSD: ~1 GB/s&lt;/li&gt;
  &lt;li&gt;Sequential read from main memory: ~100 GB/s (burst)&lt;/li&gt;
  &lt;li&gt;Sequential read from 10 Gbps Ethernet: ~1000 MB/s&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additional observations:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;~6–7 round trips per second between Europe and the US&lt;/li&gt;
  &lt;li&gt;~2000 round trips per second within a datacenter&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;back-of-the-envelope-calculations&quot;&gt;Back of the Envelope Calculations&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Quick tip:&lt;/strong&gt;
Use decimal-based approximations to simplify mental math.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;sample-calculation&quot;&gt;Sample Calculation&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt;
What is the overall latency of retrieving 30 × 256 kB images from one server?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Naïve design:&lt;/strong&gt;
Do all work on one machine → dominated by disk seek time.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;step-1-reads-required&quot;&gt;Step 1: Reads Required&lt;/h3&gt;

&lt;p&gt;30 images / 2 disks per machine = 15 reads&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;step-2-time-to-read-one-image-from-hdd&quot;&gt;Step 2: Time to Read One Image from HDD&lt;/h3&gt;

&lt;p&gt;(256 KB / 1 MB) * 5 ms + 10 ms seek
= 1.28 ms + 10 ms
= 11.28 ms&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;step-3-total-time&quot;&gt;Step 3: Total Time&lt;/h3&gt;

&lt;p&gt;15 reads * 11.28 ms = 169.2 ms&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;step-4-throughput&quot;&gt;Step 4: Throughput&lt;/h3&gt;

&lt;p&gt;1000 ms / 169.2 ms ≈ 5 result pages per second&lt;/p&gt;
</description>
				<pubDate>Wed, 15 Apr 2026 00:00:00 +0000</pubDate>
				<link>https://finbarr.ca/numbers-you-should-know/</link>
				<guid isPermaLink="true">https://finbarr.ca/numbers-you-should-know/</guid>
			</item>
		
			<item>
				<title>What, exactly, is the GIL?</title>
				<description>&lt;p&gt;Most ML code is Python. This is surprising to many performance oriented engineers coming from
non-ML communities. Python is, notably slow and has the GIL, which forces it to only execute a single
thread at a time. The GIL primarily exists to make CPython’s memory management thread-safe. It has also become a load-bearing part of Python&apos;s API, as, in a classic example of &lt;a href=&quot;https://www.hyrumslaw.com/&quot;&gt;Hyrum&apos;s Law&lt;/a&gt;, much Python code relies on the thread safety that it has created.&lt;/p&gt;

&lt;p&gt;It is also both critically important &lt;em&gt;and&lt;/em&gt; a constant source of confusion for many Python developers, including myself! To refresh my understanding, I wrote this article.&lt;/p&gt;

&lt;p&gt;In short, the Global Interpreter Lock (GIL) is a lock that exists within the CPython interpreter, guaranteeing that only one thread is executing Python bytecode at any given time. This prevents multiple threads from running Python bytecode in parallel. Note the language: &quot;Python bytecode in parallel.&quot; Pure Python is interpreted into bytecode and executed. That cannot run concurrently.&lt;/p&gt;

&lt;p&gt;Of course, much useful code requires concurrent execution, so this is a big problem. The GIL can be released, however, by using the Python C API, which provides primitives to explicitly release it. So the GIL is released by code which calls the relevant bits from the Python C API to release it.&lt;/p&gt;

&lt;p&gt;A common refrain in Python is that &quot;I/O releases the GIL.&quot; This is because all of Python&apos;s blocking I/O primitives release the GIL (i.e. call the relevant bits of the Python C API!) while waiting for the I/O block to resolve. There&apos;s nothing magical about I/O here! It&apos;s simply a (relatively) slow operation implemented in a lower-level language which releases the GIL.&lt;/p&gt;

&lt;p&gt;There are many other slow operations implemented in a lower-level language that release the GIL.&lt;/p&gt;

&lt;h2 id=&quot;when-does-torch-release-the-gil&quot;&gt;When does Torch release the GIL?&lt;/h2&gt;

&lt;p&gt;From one of the maintainers: &lt;a href=&quot;https://discuss.pytorch.org/t/can-pytorch-by-pass-python-gil/55498&quot;&gt;&quot;We do release the GIL as soon as we get out of python code. So for pytorch ops, it’s more or less all of them. The backward, unless you implement custom Functions, will run completely out of the GIL.&quot;&lt;/a&gt;. If we look at a typical PyTorch training loop:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;for batch, (X, y) in enumerate(train_dataloader): # Typically heavy Python code, holds the GIL.
    logits = model(X) # GIL is released during a bunch of the operations here.
    loss = loss_fn(logits, y) # GIL is released during a bunch of the operations here.

    optimizer.zero_grad() # Backend ops should release GIL.
    loss.backward() # Assuming it uses autograd, almost entirely outside of the GIL.
    optimizer.step() # Much of the ops that make up the bulk of the work here happen outside of the GIL.

    if batch % 100 == 0: # Python, holds the GIL.
        print(f&quot;loss: {loss.item():&amp;gt;7f}&quot;) # Python, holds the GIL (and `loss.item()` triggers a host-device sync).
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Of course, all of the Python code executing between the pytorch ops will hold the GIL.&lt;/p&gt;

&lt;h2 id=&quot;when-does-ray-release-the-gil1&quot;&gt;When does Ray release the GIL?[^1]&lt;/h2&gt;

&lt;p&gt;Calls to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ray.get&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ray.wait&lt;/code&gt; will typically release the GIL, although it depends on the specifics, and it&apos;s not guaranteed. The concrete call chain for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ray.get&lt;/code&gt; is:&lt;/p&gt;

&lt;p&gt;public &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ray.get(...)&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;worker.py&lt;/code&gt;
which calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;worker.get_objects(...)&lt;/code&gt;
which calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.core_worker.get_objects(...)&lt;/code&gt;
whose Cython implementation in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_raylet.pyx&lt;/code&gt; wraps the underlying &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CCoreWorkerProcess.GetCoreWorker().Get(...)&lt;/code&gt; call in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;with nogil:&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a typical example of the GIL! The GIL is released when you call Python code which ends up calling native code which calls the relevant bits of the Python C API to release the GIL. There&apos;s no magic!&lt;/p&gt;

&lt;p&gt;This is why a typical pattern that I like to employ in async RL works; I like to have a single process orchestrating my job using Ray, and I&apos;ll spin up a bunch of threads on the process to handle other operations. Because the threads are constantly making calls to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ray.get&lt;/code&gt;, they&apos;re constantly releasing and acquiring the GIL, so they work well together and don&apos;t starve each other of resources.&lt;/p&gt;

&lt;h2 id=&quot;when-does-vllm-release-the-gil&quot;&gt;When does vLLM release the GIL?&lt;/h2&gt;

&lt;p&gt;vLLM is largely a Python orchestrator around heavy accelerator operations. The GIL is released whenever threads execute:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;PyTorch tensor ops (ATen / CUDA kernels)&lt;/li&gt;
  &lt;li&gt;NCCL communication calls&lt;/li&gt;
  &lt;li&gt;Custom CUDA kernels (PagedAttention, FlashAttention, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are implemented in native code and explicitly release the GIL in the same way as Torch (because they mostly are Torch). So, conveniently, you can have multiple threads running concurrently on your vLLM server, perhaps executing tool calls.&lt;/p&gt;

&lt;p&gt;[1]: Funnily enough, if you google &quot;when does Ray release the GIL?&quot; one of the top results is from the Beyblade wiki.&lt;/p&gt;
</description>
				<pubDate>Fri, 10 Apr 2026 00:00:00 +0000</pubDate>
				<link>https://finbarr.ca/what-is-the-gil/</link>
				<guid isPermaLink="true">https://finbarr.ca/what-is-the-gil/</guid>
			</item>
		
			<item>
				<title>Making RL Fast</title>
				<description>&lt;p&gt;For Olmo 3, I was put in charge of our post-training infrastucture.
We made the decision to move from a synchronous RL setup to an
asynchronous one to enable us to scale. In doing that work,
I was fortunate enough to find a series of optimizations which made our RL setup 4x faster. As
we used roughly 250k H100 hours running RL on Olmo 3, these optimizations saved us approximately
750k H100 hours (~$1.5M) at current market prices. These changes
were detailed in the &lt;a href=&quot;https://arxiv.org/abs/2512.13961&quot;&gt;paper&lt;/a&gt;, but I wanted to write more about them here.&lt;/p&gt;

&lt;p&gt;There were three primary optimizations:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Continuous batching.&lt;/li&gt;
  &lt;li&gt;Inflight updates.&lt;/li&gt;
  &lt;li&gt;Better thread synchronization.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;continuous-batching-11-throughput&quot;&gt;Continuous batching (+11% throughput)&lt;/h2&gt;

&lt;p&gt;For Olmo 2, our post-training code used a synchronous RL approach,
in which the actors and learner all operate in lockstep. For every training step, we load a batch of prompts that
we want to send to the actors to complete, and we split the batch into N chunks (where N is the number of actors), sending each to a different actor.
Then, our learner waits for all the actors to complete their generations. When they&apos;re all done, the N completion chunks are
reassembled into a batch of completions which is trained on.&lt;/p&gt;

&lt;p&gt;Such a synchronous RL setup is difficult to optimize and leaves a lot of performance on the table. It does what
is known as &quot;static batching.&quot; In LLM inference, due to variable sequence length, there&apos;s a natural tendency towards
inefficiency. In traditional ML inference (say: you&apos;re running inference on a ResNet that is trying to classify MNIST),
there&apos;s no notion of sequence length, so all you have to worry about is your batch size. To efficiently run inference,
you&apos;d fill your GPU with a &lt;a href=&quot;https://www.artfintel.com/p/how-does-batching-work-on-modern&quot;&gt;sufficiently big batch&lt;/a&gt;
of examples and be done. With LLM inference, due to variable sequence length, this is inherently inefficient. If you
follow the naive approach, you&apos;ll waste &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(max_sequence_length - mean_sequence_length)/max_sequence_length&lt;/code&gt; of
your compute. On Olmo 3, we had an average generation length of 14k, and a maximum of 32k, so we would have
wasted 54\% of our compute with static batching. See the diagram for an illustrated example.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/continuous-batching.png&quot; alt=&quot;Continuous batching vs static batching&quot; style=&quot;width: 78%;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Instead, to optimally use your GPUs, you should be streaming examples in and out of your GPUs as previous completions
finish. This is called &lt;em&gt;continuous batching&lt;/em&gt;, where you constantly enqueue new generations as each one
finishes. Continuous batching is how most LLM inference servers operate, so it wasn&apos;t that difficult to
implement, but it required &lt;a href=&quot;https://github.com/allenai/open-instruct/pull/998&quot;&gt;refactoring our codebase&lt;/a&gt; to support
the new streaming architecture.&lt;/p&gt;

&lt;p&gt;(I made ChatGPT make the original version of this image in Tikz. It was remarkably difficult. The LaTeX is &lt;a href=&quot;https://gist.github.com/finbarrtimbers/153dae1598b17c75a9690cd848e86818&quot;&gt;available&lt;/a&gt; if you care to see how I tortured ChatGPT.)&lt;/p&gt;

&lt;h2 id=&quot;inflight-updates-117-throughput&quot;&gt;Inflight updates (+117% throughput)&lt;/h2&gt;

&lt;p&gt;The major disadvantage of asynchronous RL is that it makes the actors more off-policy from the learner, i.e. the
learner will be training on the current version of the model, but the actors might have generated the completions
with an older version of the model. To avoid this, the synchronous version of the code would synchronize the
actor weights after every training step. In the asynchronous setting, what this looks like is: each actor
finishes all of their enqueued generations, stops accepting new prompts to their queue, updates their copy
of the weights, and then resumes processing. Note, however, that this is exactly equivalent to the static
batching case above, and introduces the same idleness problem!&lt;/p&gt;

&lt;p&gt;Instead, we implemented &lt;a href=&quot;https://arxiv.org/abs/2509.19128&quot;&gt;PipelineRL&lt;/a&gt;, and only &lt;em&gt;pause&lt;/em&gt; generation, instead of
waiting for all completions to finish before updating the weights. By doing this, we avoid draining the queue,
and keep our nice continuous batching performance, keeping our GPUs humming. This enables a significant
increase in throughput: up to 2x faster in my experiments with the same resources, without hurting accuracy.&lt;/p&gt;

&lt;p&gt;It is, however, a bit weird. Note that we pause partway through completing a generation, and then resume it
with updated weights. We don&apos;t invalidate the KV cache! This means that we are using the KV cache calculated
from an older version of the weights. It works fine, and can be justified with a handwavy epsilon-delta proof as
a neural network is the composition of continuous functions, but it&apos;s a bit weird. Empirically it&apos;s fine, though,
and it&apos;s hard to argue with a 2x speedup.&lt;/p&gt;

&lt;h2 id=&quot;better-threading-39-throughput&quot;&gt;Better threading (+39% throughput)&lt;/h2&gt;

&lt;p&gt;Finally, after all these sophisticated changes to our RL pipeline, we ran headfirst into &lt;a href=&quot;https://en.wikipedia.org/wiki/Amdahl%27s_law&quot;&gt;Amdahl&apos;s law&lt;/a&gt;.
As we made our actors more asynchronous, the synchronization points become more of a bottlenck. In particular,
coordinating the actors, and specifically, the weight sync, was a bottleneck.&lt;/p&gt;

&lt;p&gt;To fix this, we had to completely decouple the actors, allowing each one to start and stop by itself,
without waiting for the rest of the actors to finish their syncs as well. Similarly, we make a large number
of optimizations that were not machine learning specific, and were centered around
efficiently using the CPU. For example, our initial implementation of continuous batching, for instance, was
slower than static batching until we adding a background prefetch thread to our actors that constantly refilled the inference
queue.&lt;/p&gt;

&lt;p&gt;Decoupling the actors was a pure systems change that affected how we synchronize the learner and the actors.
The idea is that if you have N actors and need to do a weight broadcast, the naive way is something like this:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Ask them all to stop (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for i in range(num_actors): actors[i].stop()&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;Send the weight update to each one in turn (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for i in range(num_actors): actors[i].update_weights()&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;Restart inference on the actors (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for i in range(num_actors): actors[i].start()&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In pseudo-code:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;for i in range(num_actors):
    actors[i].stop()
for i in range(num_actors):
    actors[i].update_weights()
for i in range(num_actors):
    actors[i].start()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If we do this, we have a bunch of synchronization points which kill performance in distributed systems, as the actors have to wait for all of the stragglers to finish.&lt;/p&gt;

&lt;p&gt;Instead, we can have each actor operate independently and have each actor start/stop independently:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;for i in range(num_actors):
    actors[i].stop()
    actors[i].update_weights()
    actors[i].start()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Of course, we run this concurrently, so all the actors are doing this simultaneously. So perhaps something closer to:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;async def update_weight(actors: list[Actor], actor_index: int):
    actor = actors[actor_index]
    await actor.stop()
	await actor.update_weights()
	await actor.restart()

await asyncio.gather(*(update_weight(actors, i) for i in range(num_actors)))
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The main bottleneck now is that we have our learner broadcasting the weight updates, which is inherently sequential. We &lt;em&gt;could&lt;/em&gt; move to an asynchronous approach like Cursor did for &lt;a href=&quot;https://arxiv.org/abs/2603.24477&quot;&gt;Composer 2&lt;/a&gt;, where the weights are written to disk, but we haven&apos;t found the weight broadcast to be a bottleneck in practice. Here&apos;s the table from the &lt;a href=&quot;https://arxiv.org/abs/2512.13961&quot;&gt;Olmo 3 paper&lt;/a&gt; showing our results.&lt;/p&gt;

&lt;p&gt;To benchmark the changes, we used Qwen 2.5 7B, generating a batch of 16 prompts, each sampled 4 times, with a maximum generation length of 32k tokens and a maximum prompt length of 2k tokens. We run each RL experiment for 2 hours and measure tokens/second based on the total output over the 2 h. We use 2 8x H100 nodes: one for the trainer and one for the actors. We have 8 actors running; one on each GPU.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Tokens per second&lt;/th&gt;
      &lt;th&gt;MFU&lt;/th&gt;
      &lt;th&gt;MBU&lt;/th&gt;
      &lt;th&gt;Notes&lt;/th&gt;
      &lt;th&gt;Commit&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;881&lt;/td&gt;
      &lt;td&gt;0.30%&lt;/td&gt;
      &lt;td&gt;12.90%&lt;/td&gt;
      &lt;td&gt;Baseline before any major changes&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/allenai/open-instruct/commit/564f8a4dd72b9298de88f8fa7c242dac53141b88&quot;&gt;564f8a4&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;975&lt;/td&gt;
      &lt;td&gt;0.33%&lt;/td&gt;
      &lt;td&gt;14.29%&lt;/td&gt;
      &lt;td&gt;plus continuous batching&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/allenai/open-instruct/pull/998&quot;&gt;ab90d5a&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1358&lt;/td&gt;
      &lt;td&gt;0.46%&lt;/td&gt;
      &lt;td&gt;19.89%&lt;/td&gt;
      &lt;td&gt;plus better threading&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/allenai/open-instruct/pull/1054&quot;&gt;e320ff0&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2949&lt;/td&gt;
      &lt;td&gt;1.01%&lt;/td&gt;
      &lt;td&gt;43.21%&lt;/td&gt;
      &lt;td&gt;plus inflight updates&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/allenai/open-instruct/pull/1054&quot;&gt;e320ff0&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
</description>
				<pubDate>Fri, 03 Apr 2026 00:00:00 +0000</pubDate>
				<link>https://finbarr.ca/making-rl-fast/</link>
				<guid isPermaLink="true">https://finbarr.ca/making-rl-fast/</guid>
			</item>
		
			<item>
				<title>The Bitter Lesson</title>
				<description>&lt;p&gt;&lt;a href=&quot;http://www.incompleteideas.net/IncIdeas/BitterLesson.html&quot;&gt;The Bitter Lesson&lt;/a&gt; is an excellent essay which is overwhelmingly misunderstood. The point of the bitter lesson is that, over time, methods which scale with compute will outperform methods that do not.&lt;/p&gt;

&lt;p&gt;It is not:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The idea that we should never incorporate human knowledge&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The idea that deep learning and scale are all we need (Rich is actually relatively skeptical of deep learning)&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire point of the essay is that, in the last 5 decades, we have seen massive increases in the amount of compute available to us as an industry and we expect to continue to see &lt;a href=&quot;https://openai.com/index/announcing-the-stargate-project/&quot;&gt;massive increases&lt;/a&gt; in the amount of compute available to AI research. Methods which take advantage of compute will benefit, and those that do not will suffer.&lt;/p&gt;

&lt;p&gt;The reason the lesson is bitter is that it is often much easier and quicker to get results by incorporating human knowledge.&lt;/p&gt;

&lt;p&gt;If you’re training an autocomplete system in 1995, you’re probably not going to get very far with next token prediction, and instead, handcoded, or statistically generated rules will do better. In 2005, N-gram models are optimal. It isn’t until the mid 2010s that we start to see deep learning dominate in NLP, and not until the late 2010s that self supervised learning becomes dominant. At each point along the way, incorporating human knowledge has been advantageous, and has been a way that you can get an advantage over your competition. But in the long term, it’s a dead end. Methods which take advantage of more compute outperform over a sufficiently long time frame. Compute is the only parameter which we can expect to increase by several orders of magnitude. Much as I wish it were otherwise, it’s unlikely that we’ll see 1000x the number of tokens we have now. But in compute, that’s almost certain.&lt;/p&gt;

&lt;p&gt;The canonical example is computer chess. Before &lt;a href=&quot;https://en.wikipedia.org/wiki/Deep_Blue_\(chess_computer\)&quot;&gt;Deep Blue&lt;/a&gt; expert systems were largely used. Deep Blue showed that leveraging compute to perform extensive searches against a hand coded value function&lt;a href=&quot;https://www.artfintel.com/p/the-bitter-lesson#footnote-1-166870478&quot;&gt;1&lt;/a&gt; performed extremely well. Deep Blue was a massive win for the “scale compute”/computer search crowd, as it was much more based on scale than human heuristics, but it required an evaluation function with 8000 custom chess features created by human experts, and the evaluation function weighted them using hand selected weights. One measure of the generality of the system is how easy it would be to extend it to a different scenario. To extend Deep Blue to work in Go would be extremely challenging, as one would need to come up with a proper evaluation function by creating another 8000 custom Go features.&lt;/p&gt;

&lt;p&gt;Computer Go is another example where human knowledge fell short. &lt;a href=&quot;https://arxiv.org/abs/1712.01815&quot;&gt;AlphaGo Zero&lt;/a&gt; evaluated against the then state of the art Go bots, which included &lt;a href=&quot;https://pasky.or.cz/go/pachi-tr.pdf&quot;&gt;Pachi&lt;/a&gt;, &lt;a href=&quot;https://www.moderndescartes.com/essays/gnugo_to_agz/&quot;&gt;GnuGo&lt;/a&gt;, and &lt;a href=&quot;https://en.wikipedia.org/wiki/Crazy_Stone_\(software\)&quot;&gt;CrazyStone&lt;/a&gt;. Pachi and CrazyStone did MCTS with heuristic value functions, and GnuGo was an expert system, with a hand created decision tree to select moves. They were good at the time! But they were all ultimately dead ends. As Rich states in the article:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The bitter lesson is based on the historical observations that&lt;/p&gt;

  &lt;p&gt;1) AI researchers have often tried to build knowledge into their agents,&lt;/p&gt;

  &lt;p&gt;2) this always helps in the short term, and is personally satisfying to the researcher, but&lt;/p&gt;

  &lt;p&gt;3) in the long run it plateaus and even inhibits further progress, and&lt;/p&gt;

  &lt;p&gt;4) breakthrough progress eventually arrives by an opposing approach based on scaling computation by search and learning.&lt;/p&gt;

  &lt;p&gt;The eventual success is tinged with bitterness, and often incompletely digested, because it is success over a favored, human-centric approach.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you look at &lt;a href=&quot;https://github.com/lungzeeyim/GNUgo&quot;&gt;GnuGo’s code&lt;/a&gt;, it was a lot of hard work by a lot of people, and was dramatically worse than what was possible. What’s surprising is that, while GnuGo &lt;a href=&quot;https://www.gnu.org/software/gnugo/devel.html&quot;&gt;began in 1989&lt;/a&gt;, released continued until 2009, so the authors were undoubtedly aware of Deep Blue and the stunning victory that scaled search had, yet they continued to push forward with their expert system. Brian Lee, a former Google Brain researcher who &lt;a href=&quot;https://github.com/tensorflow/MiniGo&quot;&gt;replicated AlphaGo within Brain&lt;/a&gt;, offers a &lt;a href=&quot;https://www.moderndescartes.com/essays/gnugo_to_agz/&quot;&gt;compelling explanation&lt;/a&gt; for why:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I offer another point: that these stages [of the Bitter Lesson] happen over the span of a decade or so. Over this decade, PhDs are minted, career identities built, promotion criteria set, culture defined, and org charts annealed. Much in the way that science progresses one funeral at a time, progress on difficult problems progresses one organization shutdown at a time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Consider another scenario. You work at a LLM lab, and have to make your benchmark numbers bigger than your competition or you get fired. You have the immediate temptation to include human knowledge, which in this case might be specialized datasets for a specific benchmark.&lt;/p&gt;

&lt;p&gt;A better approach would be to make the model &lt;em&gt;generally&lt;/em&gt; stronger. Focusing on methods that scale with compute as a filter is a strong bet to make, as Jensen Huang is doing his best to give you multiple orders of magnitude more FLOPS. Methods like test time compute, synthetic data, or MoE models are great examples. But the problem with this approach, which when I write it down seems obvious, is that in the moment, it feels &lt;em&gt;indulgent&lt;/em&gt;. We &lt;em&gt;don’t have time&lt;/em&gt; for proper science, we have to beat the other labs on LiveCodeBench. That is the bitter lesson: DeepSeek focuses on general improvements, gets them working, scales them to 3.8e25 FLOPS, and is SOTA.&lt;/p&gt;

&lt;h2 id=&quot;articles-im-reading-right-now&quot;&gt;Articles I’m reading right now:&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://substack.com/home/post/p-166556899&quot;&gt;What comes next, by Nathan Lambert (Interconnects)&lt;/a&gt;, discussing, among other aspects, how excellent O3 is.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://substack.com/home/post/p-158907079?source=queue&quot;&gt;Undertrained tokens in R1&lt;/a&gt;, by &lt;a href=&quot;https://open.substack.com/users/217957273-sander-land?utm_source=mentions&quot;&gt;Sander Land&lt;/a&gt;.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;a href=&quot;https://x.com/finbarrtimbers/status/1938115342427165035&quot;&gt;Deep Blue paper&lt;/a&gt;, which is worth reading.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href=&quot;https://www.artfintel.com/p/the-bitter-lesson#footnote-anchor-1-166870478&quot;&gt;1&lt;/a&gt; Deep Blue is fascinating for a variety of reasons, including the fact that they had custom “chess chips” made to encode the evaluation function in hardware.&lt;/p&gt;
</description>
				<pubDate>Thu, 26 Jun 2025 00:00:00 +0000</pubDate>
				<link>https://finbarr.ca/the-bitter-lesson/</link>
				<guid isPermaLink="true">https://finbarr.ca/the-bitter-lesson/</guid>
			</item>
		
			<item>
				<title>Request for research: Monte Carlo Tree Search for reasoning, with PUCT</title>
				<description>&lt;p&gt;In the recent wave of research studying reasoning models, by which we means models like O1 which are able to use long streams of tokens to &quot;think&quot; and thereby generate better results, MCTS has been discussed a lot as a potentially useful tool. However, some papers, like the &lt;a href=&quot;https://github.com/deepseek-ai/DeepSeek-R1/blob/main/DeepSeek_R1.pdf&quot;&gt;DeepSeek R1 paper&lt;/a&gt;, have tried MCTS without any success.&lt;/p&gt;

&lt;p&gt;I worked closely with MCTS for several years while at DeepMind, and there are a number of implementation details that I think researchers (such as DeepSeek) are either getting wrong or not discussing clearly. Here, I will discuss these, and issue a &lt;strong&gt;Request for Research&lt;/strong&gt; in the hopes that some intrepid researcher will implement these so I can finally stop &lt;del&gt;complaining&lt;/del&gt; suggesting that someone study this.&lt;/p&gt;

&lt;h2 id=&quot;use-puct-and-not-uct&quot;&gt;Use PUCT, and not UCT&lt;/h2&gt;

&lt;p&gt;In the DeepSeek paper, they had a section discussing MCTS:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/deepseek-r1-mcts.png&quot; alt=&quot;DeepSeek discussion of MCTS&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In the section, the authors said &quot;MCTS guided by a pre-trained value model.&quot; They repeated the phrase &quot;value model&quot; repeatedly, concluding that &quot;while MCTS can improve performance during inference when paired with a pre-trained value model, iteratively boosting model performance through self-search remains a significant challenge.&quot; To me, the phrasing indicates that the authors are not using a learned prior function, as AlphaGo/Zero/MuZero did. If true, this is a mistake.&lt;/p&gt;

&lt;p&gt;In classic MCTS, an action-value function ( $Q(s, a)$) is used to estimate the value of each candidate action:&lt;/p&gt;

\[UCT(s,a) = Q(s,a) + c \sqrt{\frac{\ln N(s)}{N(s,a)}} = Q(s, a) + c E_{\text{uct}}(s, a)\]

&lt;p&gt;In the UCT equation, there are two terms: $Q(s, a)$ and the exploration term, which is the right side of the equation ($c$ is a hyper-parameter), and is solely a function of how many times the (state, action) pair has been visited. If the current node has been visited a small fraction of the times that the parent node N(s) has been visited, the exploration term is large, but it grows smaller as it is visited more.&lt;/p&gt;

&lt;p&gt;In AlphaGo/AlphaZero/MuZero style MCTS, they use PUCT, where the exploration term is weighted by a learned prior function:&lt;/p&gt;

\[PUCT(s,a) = Q(s,a) + c P(s,a) \frac{\sqrt{N(s)}}{1 + N(s,a)} = Q(s, a) + c E_{\text{puct}}(s, a)\]

&lt;p&gt;The weight is learned by having the network predict $N(s, a)/N(s)$, i.e. the % of times that the child node is visited during search.
By using the prior, MCTS is able to go much deeper. Consider chess, which has, on average,
&lt;a href=&quot;https://en.wikipedia.org/wiki/Branching_factor&quot;&gt;35 legal moves at any point in the game&lt;/a&gt;. If you are doing N searches, and you visit
every legal move at a given state before moving on to a child state, you will only be able to explore $N^{\frac{1}{35}}$, which is tiny.
Most of these moves are obviously bad, so by using the prior to prune those nodes, search goes much deeper.&lt;/p&gt;

&lt;p&gt;Consider also the form of the exploration term. $E_{\text{uct}}(s, a)$ assigns infinite weight to a (state, action) pair that hasn&apos;t been visited before,
while $E_{\text{puct}}(s, a)$ assigns it a value of 0 (or, in some implementations, different fixed values to allow for e.g. optimistic initialization).&lt;/p&gt;

&lt;p&gt;As such, UCT will do a breadth first search, while PUCT will perform a depth-first search. Neither is superior to the other in a general sense, but in a domain
that has a large number of potential actions to take, like, say, language modelling, breadth-first search will not do much of anything.&lt;/p&gt;

&lt;p&gt;My suggestion would be to use the standard logit head as the prior and train a value head on the same embeddings that the logit head gets.
Then, update this during the RL phase of training. While the experiments are inherently expensive, you can do the experiments on a small model, such as Llama 1B,
to see if they help.&lt;/p&gt;

&lt;h2 id=&quot;parallel-evaluations-are-required-for-action-diversity&quot;&gt;Parallel evaluations are required for action diversity&lt;/h2&gt;

&lt;p&gt;With MCTS, it is very easy to harm the diversity of your search if you don&apos;t search in parallel.
If using sequential search, make sure that you are using virtual visit counts as done in the &lt;a href=&quot;https://deepmind.google/research/publications/139455/&quot;&gt;&quot;Mastering Board Games by External and Internal Planning with Language Models&quot;&lt;/a&gt; paper, where they state:&lt;/p&gt;

&lt;p&gt;&quot;Parallel MCTS implementations usually rely on virtual losses (Chaslot et al., 2008; Mirsoleimani et al., 2017) to avoid multiple threads choosing
the same simulation paths down due to deterministic action choices.&quot;&lt;/p&gt;

&lt;p&gt;Otherwise, search in parallel. The lack of parallelism is, in my mind, a big reason why the &lt;a href=&quot;https://arxiv.org/abs/2112.03178&quot;&gt;Student of Games&lt;/a&gt; struggled to beat AlphaZero in Chess/Go.&lt;/p&gt;

&lt;p&gt;Generally the way that MCTS works is that you have two phases:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;search&lt;/li&gt;
  &lt;li&gt;action selection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During search you have another two phases:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;simulation&lt;/li&gt;
  &lt;li&gt;evaluation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An example is the &lt;a href=&quot;https://github.com/google-deepmind/open_spiel/blob/master/open_spiel/algorithms/mcts.cc#L351&quot;&gt;OpenSpiel MCTS implementation&lt;/a&gt;,
although it is single-threaded and synchronous. In the simulation phase, you are
traversing the search tree and continuing to go down the search tree until you
 find a new node to add to the tree or until you reach a terminal state. In the
  evaluation phase you are running a neural network inference to say how good
  the given state is. Both of these can be done asynchronously and in parallel.&lt;/p&gt;

&lt;p&gt;So, you have some number of threads running simulations in parallel and each
  of them is queuing up evaluations which themselves are evaluated in parallel by a
  separate threadpool. The literature has shown that the exact number of threads used
   for both is critical and doing these asynchronously is also critical; both should be considered hyperparameters.&lt;/p&gt;

&lt;p&gt;When you are implementing the search tree you should make sure not to use locks.
    You want to use locks only when you are actually adding to the search tree.
    For the search tree itself, use atomics or some sort of structure that lets you
     add or modify the search statistics concurrently.&lt;/p&gt;

&lt;p&gt;When you reach the action selection phase there&apos;s a bunch of different rules that you can use to choose the action that you&apos;re actually going to take
(there&apos;s obviously any number of action selection rules you could use, but here are 3 I&apos;ve found useful):&lt;/p&gt;

&lt;p&gt;1) Normalize the search visits and sample from the resulting probability distribution
2) Softmax the visit counts and sample from the resulting probability distribution
3) Choose the most visited action during search.&lt;/p&gt;

&lt;p&gt;In the &lt;a href=&quot;https://arxiv.org/abs/2004.09677&quot;&gt;approximate best response paper&lt;/a&gt;, my experience was that
some games, particularly the stochastic games, required the softmax rule to get the
best response, while others (particularly chess/go) required the &quot;most visited&quot; rule to get the best response.&lt;/p&gt;
</description>
				<pubDate>Tue, 21 Jan 2025 00:00:00 +0000</pubDate>
				<link>https://finbarr.ca/request-for-research-puct/</link>
				<guid isPermaLink="true">https://finbarr.ca/request-for-research-puct/</guid>
			</item>
		
			<item>
				<title>RESP advice</title>
				<description>&lt;p&gt;A few people have asked me for RESP advice, so here is my generic answer, with the disclaimer that I&apos;m not a financial advisor.&lt;/p&gt;

&lt;p&gt;There are two major decisions to make for an RESP:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Decide how much you want to invest per year&lt;/li&gt;
  &lt;li&gt;Decide whether you want to self-manage your money or not&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This article discusses the options for 1. It really depends on how much money you have available to invest and what your alternatives are. I&apos;m assuming you&apos;ve maxed out your RRSP/TFSA: if you haven&apos;t, you probably want to contribute the minimum $2500 per year to max the gov&apos;t RESP grant and put the rest in your RRSP/TFSA. If you contribute at least $2500 per year for 14 years, and then have an additional year where you contribute at least $1k, you&apos;ll max out the CESG, which is the gov&apos;t grant you get for contributing to the RESP.&lt;/p&gt;

&lt;p&gt;In terms of choosing how much to invest, the mathematically optimal thing to do is to invest all of the money (~$50k) in the first year. This is, obviously, hard to do, but will result in the best outcome, as the money has the longest time horizon to grow tax-free. You don&apos;t get much gov&apos;t grants by doing this, but it&apos;s a good way to maximize your&lt;/p&gt;

&lt;p&gt;A second option is to invest $16500 ($14k + $2500 annual contribution) upfront, and then invest an additional $2500 every year until they turn 14, at which point you&apos;ve maxed out all the gov&apos;t grants, and you can top it up with $1k.&lt;/p&gt;

&lt;p&gt;Finally, if that&apos;s not financially feasible, you can do $2500 per year, and try to catch up the remaining $14k when you have extra income. In any case, it&apos;s worth doing the minimum $2500 to max out the grants.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Self-management.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A lot of the RESPs that are actively advertised to parents are, to be blunt, scams. I&apos;d recommend going with either a low-fee online broker, like Questrade, QTrade, or WealthSimple, or with one of the big bank brokerage accounts, like TD Direct Investing or BMO Investorline. Basically, whoever you have your RRSP/TFSA with should be able to open an RESP up with them.&lt;/p&gt;

&lt;p&gt;The recommended strategy for investing your money is to start out with a 90/10 equities/fixed income portfolio, something like VFV/VAB, and every year, adjust the portfolio so that when they turn 18, you have 100% in fixed income. You basically make the % allocated to fixed income equal to the (kid&apos;s age + 2) * 5%. So 10% in the first year, 15% when they&apos;re 1, 20% when they&apos;re 2, etc. You can be slightly more aggressive with your schedule, but you really want to be gradually shifting the portfolio towards 100% bonds by the time they&apos;re 18 to minimize variance.&lt;/p&gt;

&lt;p&gt;This is pretty easy to do if you&apos;re comfortable buying/selling ETFs. I have a spreadsheet that calculates this.&lt;/p&gt;

&lt;p&gt;If you&apos;re not comfortable doing this yourself, I&apos;d go with one of the managed funds. I like JustWealth as they provide a dedicated RESP target-date fund which will do everything I described above for you. You could also go with Questwealth or WealthSimple and adjust the aggressiveness over time. Looking at Questwealth as an example (but all the advisors have similar options), I&apos;d start out in an Aggressive fund for the first 4 years, then Growth for the next 4 years, followed by Balanced for 4 years, then Income for 4 years, and ending in Conservative. This would be easy to do, just set a calendar reminder to lower the aggressiveness every 4 years.&lt;/p&gt;
</description>
				<pubDate>Fri, 08 Nov 2024 00:00:00 +0000</pubDate>
				<link>https://finbarr.ca/resp-advice/</link>
				<guid isPermaLink="true">https://finbarr.ca/resp-advice/</guid>
			</item>
		
			<item>
				<title>Installing Docker on a new VM</title>
				<description>&lt;p&gt;I consistently run into the same issue when install Docker on a new Ubuntu VM.&lt;/p&gt;

&lt;p&gt;I copy the command from the &lt;a href=&quot;https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository&quot;&gt;Docker docs&lt;/a&gt;,
and it fails with the error:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;E: The repository &apos;https://download.docker.com/linux/ubuntu bookworm Release&apos; does not have a Release file.
N: Updating from such a repository can&apos;t be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There is a simple fix, but it is annoying to find it via Google, and it takes me ~20 minutes every time:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://forums.docker.com/t/solved-cannot-install-docker-ce-on-debian-stretch/50407/5&quot;&gt;https://forums.docker.com/t/solved-cannot-install-docker-ce-on-debian-stretch/50407/5&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I have now created this page to write it down. Google, please index this.&lt;/p&gt;

&lt;p&gt;To install Docker with GPU support:&lt;/p&gt;

&lt;p&gt;1) &lt;a href=&quot;https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository&quot;&gt;Install Docker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;2) Run the &lt;a href=&quot;https://forums.docker.com/t/solved-cannot-install-docker-ce-on-debian-stretch/50407/5&quot;&gt;Docker install fix&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;3) Install the &lt;a href=&quot;https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html&quot;&gt;Nvidia Container Toolkit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;4) Install &lt;a href=&quot;https://cloud.google.com/compute/docs/gpus/install-drivers-gpu&quot;&gt;GPU drivers&lt;/a&gt;.&lt;/p&gt;

</description>
				<pubDate>Fri, 02 Feb 2024 00:00:00 +0000</pubDate>
				<link>https://finbarr.ca/install-docker-on-ubuntu/</link>
				<guid isPermaLink="true">https://finbarr.ca/install-docker-on-ubuntu/</guid>
			</item>
		
			<item>
				<title>Five years of GPT progress --- Amii talk</title>
				<description>&lt;p&gt;I recently gave a talk at &lt;a href=&quot;https://www.amii.ca/&quot;&gt;Amii&lt;/a&gt; about the history of GPT models.&lt;/p&gt;

&lt;p&gt;The slides are &lt;a href=&quot;https://finbarr.ca/5_years_of_GPTs.pdf&quot;&gt;now online&lt;/a&gt;, as is the &lt;a href=&quot;https://www.youtube.com/watch?v=YA0pzBYAV2Q&amp;amp;list=PLKlhhkvvU8-YxMP9hjEYJTJDCaGszrJIh&amp;amp;index=7&amp;amp;t=43s&quot;&gt;recording&lt;/a&gt;.&lt;/p&gt;
</description>
				<pubDate>Mon, 29 May 2023 00:00:00 +0000</pubDate>
				<link>https://finbarr.ca/five-years-of-gpt-talk/</link>
				<guid isPermaLink="true">https://finbarr.ca/five-years-of-gpt-talk/</guid>
			</item>
		
			<item>
				<title>Deriving the DALL-E lower bound</title>
				<description>&lt;script type=&quot;text/x-mathjax-config&quot;&gt;
MathJax.Hub.Config({
  TeX: { equationNumbers: { autoNumber: &quot;AMS&quot; } }
});
&lt;/script&gt;

&lt;p&gt;In the &lt;a href=&quot;https://arxiv.org/abs/2102.12092&quot;&gt;DALL-E paper&lt;/a&gt;, we want to model the joint likelihood of the model distribution over images \(x\), captions \(y\), and tokens \(z\) for an encoded RGB image&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;The joint likelihood is modelled using the factorization&lt;/p&gt;

&lt;p&gt;\begin{equation}
\label{eq:factorization}
p_{\theta, \psi}(x, y, z) = p_\theta(x | y, z) p_\psi(y, z)
\end{equation}&lt;/p&gt;

&lt;p&gt;The paper presents this lower bound&lt;/p&gt;

&lt;p&gt;\begin{equation}
\label{eq:lower-bound}
\ln p_{\theta, \psi}(x, y) \geq \mathbb{E}_{z \thicksim q_\phi(z |x)} \left( \ln p_\theta (x | y, z) - D_{KL}(q_\phi(y, z | x), p_\psi(y, z))\right)
\end{equation}&lt;/p&gt;

&lt;p&gt;It was unclear to me how this was derived (and apparently &lt;a href=&quot;https://github.com/openai/DALL-E/issues/20&quot;&gt;unclear to others&lt;/a&gt;), so I thought I’d try to derive it myself.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://en.wikipedia.org/wiki/Evidence_lower_bound&quot;&gt;evidence lower bound&lt;/a&gt; lets us write&lt;/p&gt;

\[\ln p_{\theta, \psi}(x, y) \geq \mathbb{E}_{z \thicksim q_\phi}\left[ \ln \dfrac{p_{\theta, \psi}(x, y, z)}{q_\phi(z)}\right]\]

&lt;p&gt;From the factorization of the joint likelihood, we can rewrite this as&lt;/p&gt;

\[\begin{align*}
\mathbb{E}_{z \thicksim q_\phi}\left[ \ln \dfrac{p_{\theta, \psi}(x, y, z)}{q_\phi(z)}\right] &amp;amp;= \mathbb{E}_{z \thicksim q_\phi}\left[ \ln \dfrac{p_\theta(x | y, z) p_\psi(y, z)}{q_\phi(z)}\right]\\
&amp;amp;= \mathbb{E}_{z \thicksim q_\phi}\left[ \ln p_\theta(x | y, z) + \ln p_\psi(y, z) - \ln q_\phi(z)\right]\\
&amp;amp;= \mathbb{E}_{z \thicksim q_\phi}\left[ \ln p_\theta(x | y, z)\right] + \mathbb{E}_{z \thicksim q_\phi}\left[\ln p_\psi(y, z) - \ln q_\phi(z)\right]\\
&amp;amp;= \mathbb{E}_{z \thicksim q_\phi}\left[ \ln p_\theta(x | y, z)\right] + \mathbb{E}_{z \thicksim q_\phi}\left[\ln \dfrac{p_\psi(y, z)}{ q_\phi(z)}\right]
\end{align*}\]

&lt;p&gt;Note that the term on the right is precisely the KL-divergence between \(q_\phi\) and \(p_\psi\):&lt;/p&gt;

\[D_{KL}(q_\psi(y, z | x) || p_\psi(y, z)) = \mathbb{E}\left[\ln \dfrac{q_\psi(y, z | x)}{p_\psi(y, z)}\right] = -\mathbb{E}\left[\ln \dfrac{p_\psi(y, z)}{q_\psi(y, z | x)}\right]\]

&lt;p&gt;So we can write:&lt;/p&gt;

\[\mathbb{E}_{z \thicksim q_\phi}\left[ \ln \dfrac{p_{\theta, \psi}(x, y, z)}{q_\phi(z)}\right] = \mathbb{E}_{z \thicksim q_\phi}\left[ \ln p_\theta(x | y, z)\right] - D_{KL}\left(q_\psi(y, z | x) || p_\psi(y, z)\right)\]

&lt;p&gt;Which, combined with Equation \eqref{eq:lower-bound}, gives us our lower bound.&lt;/p&gt;
&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Encoded using a dVAE— see the DALL-E paper for details. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
				<pubDate>Wed, 05 Apr 2023 00:00:00 +0000</pubDate>
				<link>https://finbarr.ca/deriving-the-dall-e-lower-bound/</link>
				<guid isPermaLink="true">https://finbarr.ca/deriving-the-dall-e-lower-bound/</guid>
			</item>
		
			<item>
				<title>Five years of GPT progress</title>
				<description>&lt;p&gt;&lt;em&gt;If you want to read more of my writing, I have a &lt;a href=&quot;https://finbarrtimbers.substack.com/&quot;&gt;Substack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this article, I discuss the generative pre-trained transformer (GPT) line of work, and how it has evolved over time. I focus on the SOTA models, and the differences between them. There are a bunch of different articles summarizing these papers, but nothing that I’m aware of that explicitly focuses on the differences between them.&lt;/p&gt;

&lt;p&gt;I focus on the GPT line of research as that’s what’s driving the current fever pitch of development. There’s a ton of prior work before large GPTs (eg the &lt;a href=&quot;https://ai.googleblog.com/2006/08/all-our-n-gram-are-belong-to-you.html&quot;&gt;n-gram models&lt;/a&gt; from the 2000s, &lt;a href=&quot;https://arxiv.org/abs/1810.04805&quot;&gt;BERT&lt;/a&gt;, etc) but this post is super long, so I’m gonna save those for future articles.&lt;/p&gt;

&lt;h1 id=&quot;gpt&quot;&gt;GPT&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;https://openai.com/research/language-unsupervised&quot;&gt;Abstract&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first GPT paper is interesting to read in hindsight. It doesn’t appear like anything special and doesn’t follow any of the conventions that have developed. The dataset is described in terms of GB rather than tokens, and the number of parameters in the model isn’t explicitly stated. To a certain extent, I suspect that the paper was a side project at OpenAI and wasn’t viewed as particularly important; there’s only 4 authors, and I don’t remember it particularly standing out at the time.&lt;/p&gt;

&lt;p&gt;The architecture is remarkably unchanged compared to GPT-3:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Decoder-only transformer, with 12 layers, 768 embedding dimension, 12 attention heads, and 3072 (4x the embedding dimensions).&lt;/li&gt;
  &lt;li&gt;They use Adam, with a warm up, and anneal to 0 using a cosine schedule.&lt;/li&gt;
  &lt;li&gt;Initialize weights to N(0, 0.02), using BPE with a vocab of 40000 merges.&lt;/li&gt;
  &lt;li&gt;Activations are GELUs.&lt;/li&gt;
  &lt;li&gt;Context of 512&lt;/li&gt;
  &lt;li&gt;117M parameters&lt;/li&gt;
  &lt;li&gt;Learned position embedding, not the sinusoidal ones from “Attention is all you need”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The number of parameters isn’t explicitly discussed, but appears to be roughly 120M, easily enough to fit on a single V100 or a standard consumer GPU (rough estimate of 120M parameters for the model, 240M for the optimizer, for 360M parameters; assuming each is a float32, then this takes up 4 bytes * 360M = 1440MB/1.4GB.&lt;/p&gt;

&lt;p&gt;They use the &lt;a href=&quot;https://huggingface.co/datasets/bookcorpus&quot;&gt;BooksCorpus&lt;/a&gt; dataset (~20M tokens), training for 100 epochs with a batch size of 64. 20M tokens is a very small dataset by modern standards, as is a batch size of 64.&lt;/p&gt;

&lt;p&gt;The most surprising thing compared to modern GPTs is that they train for 100 epochs. Modern GPTs rarely ever see repeated data, and if they do, they typically only see certain datapoints a small number of times (2-4x), and the entire dataset is never repeated 100x.&lt;/p&gt;

&lt;h1 id=&quot;gpt-2&quot;&gt;GPT-2&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;https://openai.com/research/better-language-models&quot;&gt;Abstract&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GPT-2 is where the language models start to get big&lt;em&gt;.&lt;/em&gt; This is the first time that OpenAI trains a model with &amp;gt;1B parameters. We start to see scale as a primary concern; in GPT, the authors trained a single model, but here, the authors train a range of models, with sizes ranging from GPT to 10x GPT (which is the actual GPT-2 model).&lt;/p&gt;

&lt;p&gt;The differences in architecture compared to GPT are as follows:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;They layernorm the inputs and add an additional layernorm to the output of the final self-attention block&lt;/li&gt;
  &lt;li&gt;Weights are scaled by layer by 1/sqrt(n)&lt;/li&gt;
  &lt;li&gt;Vocabulary of ~50k (up from ~40k)&lt;/li&gt;
  &lt;li&gt;Context of 1024 (up from 512)&lt;/li&gt;
  &lt;li&gt;Batches of 512 (up from 64)&lt;/li&gt;
  &lt;li&gt;Largest model is 1.5B parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dataset is much, much bigger, going from roughly 20M tokens (4GB) of data consisting of publicly available books, to 9B tokens&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; (40GB) of text scraped from the internet (&lt;a href=&quot;https://paperswithcode.com/dataset/webtext&quot;&gt;WebText&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;It’s unclear if they trained the model for 100 epochs as before; they say they followed the same training procedure, so presumably they did. Again, this is a significant departure from later work.&lt;/p&gt;

&lt;p&gt;Nothing here is particularly different from GPT; most of the changes are related to making the model bigger. The only other changes are the layernorm changes and the weight scaling, which don’t seem to make a big difference (although, as always, more ablations would be nice).&lt;/p&gt;

&lt;h1 id=&quot;gpt-3&quot;&gt;GPT-3&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;https://arxiv.org/abs/2005.14165&quot;&gt;Abstract&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is where the era of truly &lt;em&gt;large&lt;/em&gt; language models began, and the current AI &lt;del&gt;bubble&lt;/del&gt; excitement took off. In the paper, the authors train 10 models, varying from 125M parameters (”GPT-3 Small”) to 175B parameters (”GPT-3”).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/gpt-3-models.png&quot; alt=&quot;Screenshot from the GPT-3 paper showing the various models they trained&quot; /&gt;&lt;/p&gt;

&lt;p&gt;For each of the models, the architectures are identical to GPT-2 with the exception that they use “alternating dense and locally banded sparse attention patterns in the layers of the transformer.” The sparse attention here refers to the attention mechanism introduced in the &lt;a href=&quot;https://paperswithcode.com/method/sparse-transformer&quot;&gt;Sparse Transformer&lt;/a&gt;, which lets attention scale proportional to \(O(n \sqrt{n})\) (where \(n\) is the context length). The standard dot-product attention mechanism scales proportional to \(O(n^2)\), so this is a substantial gain. I would have loved a proper ablation to see what difference sparse vs dense attention makes, but alas.&lt;/p&gt;

&lt;p&gt;I’m very curious &lt;em&gt;why&lt;/em&gt; they used sparse attention. Reproductions and later papers uniquely use dense attention. As this paper came before &lt;a href=&quot;https://arxiv.org/abs/2205.14135&quot;&gt;FlashAttention&lt;/a&gt; and some of the other algorithmic innovations that make dense attention faster, maybe this was a computational bottleneck? It’s really unclear.&lt;/p&gt;

&lt;p&gt;They don’t provide any detail about the computational architecture, i.e. how they distributed the model. The authors claim it’s because it doesn’t really matter, but I think it was restricted for competitive reasons, as it makes the paper much more difficult to reproduce. Megatron, which I’ll discuss later, was highly influential &lt;em&gt;because&lt;/em&gt; they went into detail about how they made model parallelism work for their GPT.&lt;/p&gt;

&lt;p&gt;What I find really interesting about the GPT-3 paper is that it was an incredible advance without a lot of novelty. They took their existing methods and “just” scaled it up! Because of the need for novelty, there are many research projects that don’t get pursued because they’re “only” engineering projects, or they “only” do hyper-parameter tuning and wouldn’t be able to get published, even if they had impressive performance improvements. That OpenAI went against the grain here is a credit to them (and they were rewarded, with GPT-3 getting a best paper reward at NeurIPS ‘20).&lt;/p&gt;

&lt;p&gt;This is particularly problematic because we know that adding complexity to our models increases performance (see: R^2 vs adjusted R^2 for simple linear models). Because of the need for novelty, there are many research projects that don’t get pursued because they’re “only” engineering projects, or they “only” do hyper-parameter tuning and wouldn’t be able to get published, even if they had impressive performance improvements. That OpenAI went against the grain here is a credit to them.&lt;/p&gt;

&lt;p&gt;This is a strength of OpenAI (and Stability.ai, Midjourney, basically everywhere that’s not FAIR/Google Brain/Deepmind/etc). You could alternatively frame it as a weakness of the more academic labs that have promotion/performance review policies driven by publications.&lt;/p&gt;

&lt;h1 id=&quot;jurassic-1&quot;&gt;Jurassic-1&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;https://uploads-ssl.webflow.com/60fd4503684b466578c0d307/61138924626a6981ee09caf6_jurassic_tech_paper.pdf&quot;&gt;PDF&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wasn’t sure whether or not to include Jurassic-1. It’s a model from the Israeli tech company AI21 Labs. I haven’t heard a lot about them, but the paper’s cited by a bunch of the papers later on in the article; they trained a 178B parameter model that outperformed GPT-3 in a few categories, and was faster for inference. It’s impressive that they’re competing with DeepMind, OpenAI, Nvidia, etc. despite only having &lt;a href=&quot;https://en.wikipedia.org/wiki/AI21_Labs&quot;&gt;raised &amp;lt;$$10M&lt;/a&gt; at the time. They made a zero-shot and few-shot test suite &lt;a href=&quot;https://github.com/ai21labs/lm-evaluation&quot;&gt;publicly available&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Like many other papers, they don’t go into detail about the engineering details behind training a large model (178B parameters) over 800 GPUs:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/jurassic-computational.png&quot; alt=&quot;Screenshot from Jurassic-1 paper describing how they don&apos;t need to describe the computational details&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The paper is remarkably sparse on details, which I suspect was done for competitive reasons, just like GPT-4.&lt;/p&gt;

&lt;p&gt;Facebook is the only company to go into &lt;a href=&quot;https://github.com/facebookresearch/metaseq/blob/main/projects/OPT/chronicles/OPT175B_Logbook.pdf&quot;&gt;detail about their experiences&lt;/a&gt; training a 175B parameter model, just like Nvidia is the only company to go into detail about the computational architecture required to train a LLM over many GPUs (see: the Megatron paper, next). In both cases, the companies are &lt;a href=&quot;https://gwern.net/complement&quot;&gt;commoditizing their complements&lt;/a&gt; and strengthening their main lines of business by making it easier to train large models.&lt;/p&gt;

&lt;p&gt;Jurassic uses a different architecture from GPT-3, but again, doesn’t go into much detail:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;76 layers (vs 96 layers for GPT-3)&lt;/li&gt;
  &lt;li&gt;They use the SentencePiece tokenizer, with a large vocabulary of 256K (vs GPT-3 which used BPE w/ ~50k tokens).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither of these changes are material, in my opinion. I think what we’re seeing is that there’s a relatively large degree of freedom in model architectures which produce similar results. This is borne out by their evaluation, which has results similar to GPT-3 (better in some categories, worse in others), although Jurassic-1 is faster for inference due to being shallower.&lt;/p&gt;

&lt;p&gt;We’re starting to see a consistent pattern emerge:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Papers introduce a bunch of changes, their own dataset, and have a new SOTA&lt;/li&gt;
  &lt;li&gt;but they don’t do a proper ablation, so it’s tough to understand what was important and what &lt;em&gt;drove&lt;/em&gt; the improvements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GPT-2, GPT-3, Jurassic-1, etc. all did this.&lt;/p&gt;

&lt;h1 id=&quot;megatron-turing-nlg&quot;&gt;Megatron-Turing NLG&lt;/h1&gt;

&lt;p&gt;Megatron was a highly influential paper that introduced efficient model-parallel architectures. If you’re interviewing for a LLM job today, you’re going to be expected to be familiar with it. Megatron introduced &lt;em&gt;tensor parallelism&lt;/em&gt;, a variant of model parallelism that splits the models to allow for intra-layer model parallelism, achieving 76% as efficient as a single GPU baseline (although the baseline is only 30% of peak FLOPS).&lt;/p&gt;

&lt;p&gt;Prior to megatron, the published SOTA for model parallelism was to use model pipelining, e.g. &lt;a href=&quot;https://arxiv.org/abs/1811.06965&quot;&gt;GPipe&lt;/a&gt;. However, this was difficult to do and not well supported by code. There were attempts to support tensor parallelism, e.g. &lt;a href=&quot;https://paperswithcode.com/method/mesh-tensorflow&quot;&gt;Mesh-Tensorflow&lt;/a&gt;, which introduced a language for specifying a general class of distributed computations in TensorFlow, but nothing had really dominated. Interestingly, the first author had just left DeepMind 1 year before this was published, so this was possibly his first project at Nvidia.&lt;/p&gt;

&lt;p&gt;Megatron has the realization that, if you have a neural network like this:&lt;/p&gt;

\[Y = f(XW)\]

&lt;p&gt;and you split \(W = \begin{bmatrix} W_1 &amp;amp; W_2 \end{bmatrix}\), i.e. along the columns, then \(Y = \begin{bmatrix}f(X W_1) &amp;amp; f(X W_2)\end{bmatrix}\), so you don’t need to do any synchronization to calculate \(Y\). Consequently, the only points where you need synchronization (all-reduces) in the transformer are:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;In the forward pass, to concatenate the model activations after the MLP block before adding dropout&lt;/li&gt;
  &lt;li&gt;In the backwards pass, at the start of the self-attention block.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/megatron-architecture.png&quot; alt=&quot;Graphic showing how megatron parallelism works&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Now, I strongly suspect this is what GPT-3 and Jurassic-1 both did, but neither went into detail about the specific parallelism models they used, other than to say (from GPT-3):&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;To train the larger models without running out of memory, we use a mixture of model parallelism within each matrix multiply and model parallelism across the layers of the network.&lt;/p&gt;

&lt;/blockquote&gt;

&lt;p&gt;Presumably, this style of parallelism is what is meant by “model parallelism within each matrix multiply,” as I find it hard to imagine what else they could mean.&lt;/p&gt;

&lt;h1 id=&quot;gopher&quot;&gt;Gopher&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;https://arxiv.org/abs/2112.11446&quot;&gt;Abstract&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gopher was a LLM trained by DeepMind. Interestingly, the lead author joined OpenAI shortly after it was published, along with a few of the coauthors. The architecture was the same as GPT-2, except:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;They use &lt;a href=&quot;https://arxiv.org/abs/1910.07467&quot;&gt;RMSNorm&lt;/a&gt; (instead of layernorm)&lt;/li&gt;
  &lt;li&gt;Use relative positional encoding scheme from &lt;a href=&quot;https://arxiv.org/abs/1901.02860&quot;&gt;Transformer-XL&lt;/a&gt; (while GPT-* used a learned positional embedding)&lt;/li&gt;
  &lt;li&gt;They use &lt;a href=&quot;https://arxiv.org/abs/1808.06226&quot;&gt;SentencePiece&lt;/a&gt; (instead of &lt;a href=&quot;https://en.wikipedia.org/wiki/Byte_pair_encoding&quot;&gt;BPE&lt;/a&gt;). This seems to be an Alphabet thing; many of the Alphabet papers use SentencePiece, while most of the non-Alphabet world uses BPE.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The paper was very interesting from a computational perspective, as they went into detail about how they trained their model and made it work:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;They used optimizer state partitioning (&lt;a href=&quot;https://arxiv.org/abs/1910.02054&quot;&gt;ZeRO&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.notion.so/b2293a2b125b4b088656e039fb3b6ca8&quot;&gt;Megatron-style&lt;/a&gt; model parallelism&lt;/li&gt;
  &lt;li&gt;And &lt;a href=&quot;https://paperswithcode.com/method/gradient-checkpointing#:~:text=Gradient%20Checkpointing%20is%20a%20method,small%20increase%20in%20computation%20time.&quot;&gt;rematerialization&lt;/a&gt;/gradient checkpointing to save memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are all now the standard techniques used to train large models. To the best of my knowledge, Gopher was the first paper to put all of these together and release details about doing so publicly.&lt;/p&gt;

&lt;p&gt;It’s interesting— often, big labs don’t include details for comeptitive reasons. Here, because DeepMind was (arguably) behind, they went into extensive detail. I think we’ll see this increase with LLM research from everyone that’s not OpenAI/Anthropic, as the others don’t live/die by the commercial success of their API, and have strong incentives to make it easier for &lt;strong&gt;**&lt;/strong&gt;others&lt;strong&gt;**&lt;/strong&gt; to train large models (and thereby &lt;a href=&quot;https://gwern.net/complement&quot;&gt;commoditize their complements&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For the paper, DeepMind built a dataset called MassiveText, which was as follows:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/massivetext.png&quot; alt=&quot;Screenshot from the Gopher paper describing their MassiveText dataset&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Interestingly, this is much smaller than the dataset OpenAI used for GPT-3. GPT-3 had roughly 45TB of text, while MassiveText “only” had about 10.5TB.&lt;/p&gt;

&lt;p&gt;They used this dataset to trained a large model on 300B tokens. The dataset consists of 2.343 trillion tokens, so this is only 12.8%. A much smaller subset. This is interesting to compare to the earlier GPTs, which, if you recall, used 100 epochs (so they saw each token in the dataset 100 times— while Gopher only saw 10% of their tokens once)!&lt;/p&gt;

&lt;p&gt;The Gopher appendices have some great work; someone finally did ablations! They looked at:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://paperswithcode.com/method/adafactor&quot;&gt;Adafactor&lt;/a&gt; vs Adam, and found that Adafactor was much less stable&lt;/li&gt;
  &lt;li&gt;Lower-precision training, trying runs with float16, bfloat16, float32, &lt;a href=&quot;https://nhigham.com/2020/07/07/what-is-stochastic-rounding/&quot;&gt;RandRound&lt;/a&gt;, and using bfloat16 parameters with float32 in the optimiser state (rounding randomly). They found that using float32 parameters for optimisation updates only mitigated the performance loss, saving a substantial amount of memory.&lt;/li&gt;
  &lt;li&gt;Scaling context length; they show how performance increases as the context length increases. Improvements see diminishing returns, but consistently improve. Performance looks roughly proportionate to \(\sqrt{n}\) (where \(n\) is the context length).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s really nice to see detailed empirical work like this— it’s a welcome change from the other papers that failed to do this.&lt;/p&gt;

&lt;h1 id=&quot;chinchilla&quot;&gt;Chinchilla&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;https://paperswithcode.com/method/chinchilla&quot;&gt;Abstract&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Chinchilla is an incredibly influential paper that established scaling laws. It’s one of my favorite papers from the last few years, as it &lt;em&gt;actually does science&lt;/em&gt; in a way that physicists would agree with. One answer to “is something science” is to say, if you were to meet a historical scientist in your field, could you teach them something? And if you brought Chinchilla to researchers to, say, Radford et. al in 2017, it would advance their work by several years.&lt;/p&gt;

&lt;p&gt;Chinchilla trained over 400 GPT-style transformers, ranging in size from 70M to 16B parameters, and fit the following equation (N is the number of parameters in the LM, and D is the number of tokens in the dataset):&lt;/p&gt;

\[\hat{L}(N, D) = E + \dfrac{A}{N^\alpha} + \dfrac{B}{D^\beta}\]

&lt;p&gt;Choosing \(A, B, E, \alpha, \beta\) to minimize&lt;/p&gt;

\[\sum \limits_{\text{Runs } i} \text{Huber}_{\delta=10^{-3}}(\log \hat{L}_i - \log L_i)\]

&lt;p&gt;where the Huber loss is&lt;/p&gt;

\[\text{Huber}_\delta(x) = \begin{cases}
\frac 1 2 a^2 &amp;amp; |a| \leq \delta,\\
\delta \cdot (|a| - \frac 1 2 \delta) &amp;amp; |a| &amp;gt; \delta
\end{cases}\]

&lt;p&gt;Here, we can think of E as the “irreducible loss” from the dataset, i.e. the loss if we trained an infinitely large model on an infinite stream of tokens. The authors find that the optimal model is (from &lt;a href=&quot;https://www.lesswrong.com/posts/6Fpvch8RR29qLEWNH/chinchilla-s-wild-implications#fnjefvfidovdb&quot;&gt;nostalgebraist&lt;/a&gt; on into the implications of Chinchilla):&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/chinchilla-equation.png&quot; alt=&quot;Chinchilla equation&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The implication here is that the model size &amp;amp; data size matter roughly equally, which is interesting, given how much attention &amp;amp; effort goes to scaling up the model, and how little attention is given to the dataset.&lt;/p&gt;

&lt;p&gt;The authors then used this equation to determine the optimal model size for the Gopher compute budget, and trained it on more tokens— 1.4T tokens, 4.6x the number of tokens Gopher was trained on. This model, being 4x smaller, has a radically smaller memory footprint and is much faster/cheaper to sample from.&lt;/p&gt;

&lt;p&gt;The Chinchilla paper has been highly influential. Almost every team that I’ve been talking to that is training a LLM right now talks about how they’re training a &lt;em&gt;Chinchilla optimal model&lt;/em&gt;, which is remarkable given that basically everything in the LLM space changes every week.&lt;/p&gt;

&lt;p&gt;The standard practice before Chinchilla was to train your model for 300B tokens, which is what GPT-3, Gopher, and Jurassic-1 all did. Chinchilla reveals how wasteful that was; basically, all of these papers made themselves more expensive to infer by training models that were too large.&lt;/p&gt;

&lt;p&gt;Changes from Chinchilla (otherwise the same as Gopher):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.notion.so/Factual-accuracy-issues-in-LLMs-8257c59fda4040509a40d94523701fad&quot;&gt;AdamW&lt;/a&gt; instead of Adam (there’s an interesting footnote regarding the choice of optimizer: “a model trained with AdamW only passes the training performance of a model trained with Adam around 80% of the way through the cosine cycle, though the ending performance is notably better”)&lt;/li&gt;
  &lt;li&gt;Uses a modified &lt;a href=&quot;https://arxiv.org/abs/1808.06226&quot;&gt;SentencePiece&lt;/a&gt; tokenizer that is slightly different from Gopher (doesn’t apply NFKC normalisation)&lt;/li&gt;
  &lt;li&gt;They compute the forward + backward pass in bfloat16, but store a float32 copy of the weights in the optimizer state. They find that this is basically identically efficient to using float32 everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of the changes are ablated extensively in the appendix. None of these are particularly novel.&lt;/p&gt;

&lt;h1 id=&quot;palm&quot;&gt;PaLM&lt;/h1&gt;

&lt;p&gt;Speaking of training models that were too large- we have PaLM! Palm was really, really big. &lt;a href=&quot;https://twitter.com/finbarrtimbers/status/1635102571567407105?s=46&amp;amp;t=_LCsoamG7K4pQj0vxlk0XA&quot;&gt;As far as I’m aware&lt;/a&gt;, it’s the largest dense language model trained to date,  at 540B parameters, requiring 6144 TPUs to train on (this is 3 entire TPU pods, each consisting of 2048 TPUs). This is incredibly expensive! Probably only Google has the resources + infrastructure to do this.&lt;/p&gt;

&lt;p&gt;… unfortunately, they were training PaLM at the same time chinchilla was being written. Very suboptimal.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://twitter.com/rasbt/status/1637803700944093184?s=46&amp;amp;t=_LCsoamG7K4pQj0vxlk0XA&quot;&gt;Changes from GPT-3&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1911.02150&quot;&gt;Multi-query attention&lt;/a&gt;. Shares K/V embeddings for each head, but has separate Q embeddings. Makes it much faster during inference time.&lt;/li&gt;
  &lt;li&gt;Uses &lt;a href=&quot;https://twitter.com/rasbt/status/1637803703766769669&quot;&gt;parallel transformer blocks&lt;/a&gt;, which improves training time by 15%. As it was trained using 6144 TPU v4 chips for 1200 hours, the total training cost (at public prices) is between \(1.45 to\)3.22 per chip-hour, for a total of \(10M to\)22M. So this change saved \(1.5M to\)3M.&lt;/li&gt;
  &lt;li&gt;SwiGLU activations, rather than the GELU activation used by GPT-3&lt;/li&gt;
  &lt;li&gt;RoPE embeddings, rather than the learned embeddings&lt;/li&gt;
  &lt;li&gt;Shares the input-output embeddings&lt;/li&gt;
  &lt;li&gt;No bias vectors&lt;/li&gt;
  &lt;li&gt;SentencePiece with 256k tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, a ton of changes! Again, a bunch of these are common, e.g. using the learned embeddings that GPT-3 had is very passé, and almost no one does it now.&lt;/p&gt;

&lt;h1 id=&quot;llama&quot;&gt;LLaMa&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;https://ai.facebook.com/blog/large-language-model-llama-meta-ai/&quot;&gt;Abstract&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LLaMa combined a bunch of the best feartures from PaLM and Chinchilla:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Pre-normalize the input of each transformer sub-layer&lt;/li&gt;
  &lt;li&gt;RMSNorm, instead of LayerNorm, as done in Gopher&lt;/li&gt;
  &lt;li&gt;SwiGLU activation function from PaLM (but a dimension of \(\frac{2}{3} 4d\) instead of \(4d\), as in PaLM)&lt;/li&gt;
  &lt;li&gt;Uses &lt;a href=&quot;https://arxiv.org/abs/2104.09864&quot;&gt;rotary positional embeddings&lt;/a&gt; (RoPE) instead of the absolute positional embeddings, as done in PaLM&lt;/li&gt;
  &lt;li&gt;Uses AdamW, as done in Chinchilla&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think that LLaMa is the recipe to follow for the current SOTA in training large models.&lt;/p&gt;

&lt;p&gt;Computational changes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Uses efficient attention (&lt;a href=&quot;https://arxiv.org/abs/2112.05682&quot;&gt;Rabe &amp;amp; Staats&lt;/a&gt;, &lt;a href=&quot;https://arxiv.org/abs/2205.14135&quot;&gt;FlashAttention&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2205.05198&quot;&gt;Gradient checkpointing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Interestingly, they appear to be using float32s everywhere (or at least, don’t say otherwise)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are all similar to Gopher. The one obvious optimization they missed is to use lower precision, as Chinchilla did; I’m curious why they didn’t.&lt;/p&gt;

&lt;p&gt;My one complaint is that I wish they would have trained the model for longer. The learning curve is very far from convergence! This paper is, in my mind, the shining example showing how well smaller models can do when trained well.&lt;/p&gt;

&lt;h1 id=&quot;gpt-4&quot;&gt;GPT-4&lt;/h1&gt;

&lt;p&gt;This is where I’d include information about GPT-4, if there was any. Unfortunately, the &lt;a href=&quot;https://cdn.openai.com/papers/gpt-4.pdf&quot;&gt;GPT-4 technical report&lt;/a&gt; contains almost no information:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;GPT-4 is a Transformer-style model [33] pre-trained to predict the next token in a document, using both publicly available data (such as internet data) and data licensed from third-party providers. The model was then fine-tuned using Reinforcement Learning from Human Feedback (RLHF) [34]. Given both the competitive landscape and the safety implications of large-scale models like GPT-4, this report contains no further details about the architecture (including model size), hardware, training compute, dataset construction, training method, or similar.&lt;/p&gt;

&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/gpt-4-meme.jpeg&quot; alt=&quot;Meme complaining about how OpenAI didn&apos;t relase any details about GPT-4&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As a result, I’m not going to talk about it, as there’s not much to say. Hopefully OpenAI changes their mind and releases some information about their model.&lt;sup id=&quot;fnref:2&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;This is it, as of March ‘23. I’m sure something new will come along and invalidate all of this.&lt;/p&gt;

&lt;p&gt;What have I missed? Add comments on Substack or &lt;a href=&quot;mailto:finbarrtimbers@gmail.com&quot;&gt;email me&lt;/a&gt; and I’ll update it.&lt;/p&gt;

&lt;p&gt;Articles I’m reading:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://rootnodes.substack.com/p/why-didnt-deepmind-build-gpt3&quot;&gt;Why didn’t DeepMind build GPT-3?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;The paper itself doesn’t report the number of tokens, but &lt;a href=&quot;https://skylion007.github.io/OpenWebTextCorpus/&quot;&gt;OpenWebText&lt;/a&gt;, the open source reproduction, gets &lt;a href=&quot;https://github.com/karpathy/nanoGPT/blob/master/data/openwebtext/readme.md&quot;&gt;nine billion&lt;/a&gt;, using &lt;a href=&quot;https://github.com/openai/tiktoken&quot;&gt;OpenAI’s tokenizer&lt;/a&gt;. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;To be clear, I highly doubt this will ever happen. &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
				<pubDate>Mon, 27 Mar 2023 00:00:00 +0000</pubDate>
				<link>https://finbarr.ca/five-years-of-gpt-progress/</link>
				<guid isPermaLink="true">https://finbarr.ca/five-years-of-gpt-progress/</guid>
			</item>
		
	</channel>
</rss>
