<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Erik Kaunimaki</title><description>Personal site</description><link>https://erikkaum.com/</link><atom:link href="https://erikkaum.com/rss.xml" rel="self" type="application/rss+xml"/><item><title>How to Build an ML Framework in Rust, from Scratch, in a Weekend</title><link>https://erikkaum.com/blog/zml/</link><guid isPermaLink="true">https://erikkaum.com/blog/zml/</guid><description>A deep dive into ZML, StableHLO and PJRT.</description><pubDate>Fri, 06 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;ZML is an ML framework written in Zig, and it&apos;s the only ML framework that fully focuses on inference. But &lt;em&gt;how&lt;/em&gt; does it work, and &lt;em&gt;what does it mean&lt;/em&gt; to focus on inference rather than training?&lt;/p&gt;
&lt;p&gt;I wanted to understand the stack properly, so I built a minimal version of ZML in Rust.
This post is a guided tour of what sits underneath, and how you can build your own ML framework to run a real model.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;I&apos;ll start this blog by going through how ZML works under the hood and explaining the end-to-end pipeline from model definition to StableHLO to PJRT. Once we have a solid understanding of what it should look like, we&apos;ll build a toy/educational ML framework. The first version will be ugly, and the last part of this blog is devoted to cleaning it up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What we&apos;ll cover:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#part-1-reading&quot;&gt;Part 1: Reading&lt;/a&gt; — how ZML works under the hood&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#part-2-building&quot;&gt;Part 2: Building&lt;/a&gt; — building a toy framework that emits StableHLO&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#part-3-running&quot;&gt;Part 3: Running&lt;/a&gt; — compiling and executing via PJRT&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#part-4-cleaning&quot;&gt;Part 4: Cleaning&lt;/a&gt; — making the API feel like a real framework&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#demo&quot;&gt;Demo&lt;/a&gt; — running SmolLM2-135M end to end&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The outcome of this blog is that we&apos;ll build a toy/educational ML framework that will run the SmolLM2-135M model, something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ ./target/release/examples/smollm2 chat                                                       
No compiled artifact given, compiling from scratch (seq=256)...
Compiled in 728.99ms
Loaded weights in 91.99ms

SmolLM2-135M-Instruct ready. Type a message and press Enter. Type &quot;exit&quot; to quit.

You&amp;gt; Hi 👋
Assistant&amp;gt; Hello! I&apos;m here to help with any creative writing needs. What&apos;s on your mind?
[24 prompt tokens, 19 generated | TTFT 306ms | 4.5 tok/s]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I assume you&apos;re comfortable with programming and have a basic understanding of neural networks: you know what a forward pass, a matmul, and a weight tensor are. You don&apos;t need to be an expert, but I&apos;ll cut to the chase pretty quickly.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Short intro to ZML&lt;/h2&gt;
&lt;p&gt;ZML is an ML framework for inference, written in Zig. It sits at roughly the same level as PyTorch, but it is optimized for inference only. That constraint shapes many of its design decisions and makes it feel quite different from PyTorch.&lt;/p&gt;
&lt;p&gt;To understand what ZML is trying to solve, it helps to look at where PyTorch is less natural as an inference runtime. PyTorch defaults to eager execution: ops run immediately as your Python code executes them. That is excellent for model development and experimentation, where flexibility matters most. But for production inference, you often end up introducing a second stage such as export, tracing, or compilation, and that handoff is where edge cases and operational complexity tend to appear.&lt;/p&gt;
&lt;p&gt;Inference has a different nature: the goal is not to support an interactive research workflow, but to produce a stable, reusable artifact that runs predictably in production. This is why ZML is built around a graph-compile-run pipeline: you first stage the computation as a graph, then compile that graph into an accelerator-specific executable, and finally run that executable with real inputs. In other words, the model definition is treated less like code to execute line by line, and more like a specification that can be analyzed, optimized, and compiled ahead of time.&lt;/p&gt;
&lt;p&gt;That model fits inference well, where we usually care more about predictability, startup behavior, hardware targeting, and repeatable performance than maximum flexibility at runtime.&lt;/p&gt;
&lt;p&gt;If you’re coming from a PyTorch world, three things stand out in ZML:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Graph-first&lt;/strong&gt;: the model is staged as a computation graph up front, rather than executed eagerly op by op.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model and weights are separate&lt;/strong&gt;: you can validate and compile the model structure without loading the actual weights. When weights are tens of gigabytes, that makes iteration much faster.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compiled for the target&lt;/strong&gt;: ZML is designed to compile the model into an executable for a specific backend and deployment target, which makes it feel much closer to a serving stack than a research environment.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As someone who has worked on inference for several years, this resonated with me immediately. If you’re curious how a stack like this can even be built, follow along: we’ll start with a high-level overview of ZML, then build out the core pieces ourselves.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Part 1: Reading&lt;/h1&gt;
&lt;h2&gt;The stack&lt;/h2&gt;
&lt;p&gt;There&apos;s a big stack involved here, and it&apos;s not immediately obvious what does what. But nothing here is difficult &lt;em&gt;per se&lt;/em&gt;; there&apos;s just a lot of it.&lt;/p&gt;
&lt;p&gt;The short version is:&lt;/p&gt;
&lt;p&gt;Write the model in Zig, lower it to StableHLO (an MLIR dialect), then let OpenXLA compile and run it through PJRT.&lt;/p&gt;
&lt;p&gt;Before we unpack each layer, here&apos;s the short explanation of each piece:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;StableHLO&lt;/strong&gt; is an IR (intermediate representation) that describes ML computations. It’s a dialect of &lt;strong&gt;MLIR&lt;/strong&gt;, so in many places, including this blog, they’re used interchangeably.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PJRT&lt;/strong&gt; is the runtime API that compiles and executes those computations on actual hardware.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenXLA&lt;/strong&gt; is the umbrella project that ties them together.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The top of the stack is the easiest to understand, since it feels very similar to PyTorch. You write your model in Zig as a struct:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;const Scale = struct {
    weight: zml.Tensor,

    pub fn forward(self: Scale, x: zml.Tensor) zml.Tensor {
        return self.weight.mul(x);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The struct has a weight tensor and a forward method that does an elementwise multiply. You&apos;d use it like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;const x = zml.Tensor.init(.{1, 4}, .f32);
const y = scale.forward(x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is somewhat pseudo-code, but you get the idea.&lt;/p&gt;
&lt;p&gt;The core part to understand here is that &lt;strong&gt;there&apos;s no data attached to this yet&lt;/strong&gt;. The data in the &lt;code&gt;weight&lt;/code&gt; tensor isn&apos;t here, and it doesn&apos;t need to be. In ZML, a tensor is just some shape metadata plus an MLIR value that identifies the operation that produced it.&lt;/p&gt;
&lt;p&gt;&quot;What the hell is an MLIR value?&quot; Fair question. It&apos;s tempting to first think of it as a pointer to data in memory. But it&apos;s not that, since there&apos;s no data to point to. Think of it as a &lt;em&gt;name&lt;/em&gt; for a result. It&apos;s what&apos;s called a single static assignment (SSA) value: each operation in the computation graph produces one or more results, and each result gets a unique SSA name (like &lt;code&gt;%0&lt;/code&gt;, &lt;code&gt;%4&lt;/code&gt;). The operation itself is the node in the graph; the SSA value is the handle you use to refer to its output. In essence, it tells you where a tensor came from and how it flows through the computation, but it doesn&apos;t hold actual numeric data.&lt;/p&gt;
&lt;p&gt;Let&apos;s visualize our multiplication. Both the &lt;code&gt;weight&lt;/code&gt; and the input tensor &lt;code&gt;x&lt;/code&gt; are tensors, so they get assigned an SSA value: &lt;code&gt;%0&lt;/code&gt; and &lt;code&gt;%1&lt;/code&gt;. In the forward pass, the two values are multiplied, producing a new tensor with the id &lt;code&gt;%2&lt;/code&gt;. So the new tensor with the id &lt;code&gt;%2&lt;/code&gt; is just the result of multiplying &lt;code&gt;%0&lt;/code&gt; and &lt;code&gt;%1&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/zml/1.png&quot; alt=&quot;graph&quot; /&gt;
&lt;/div&gt;
&lt;p&gt;If the MLIR value doesn&apos;t fully click yet, don&apos;t worry, it will become clearer as we go deeper and start building things ourselves.&lt;/p&gt;

What&apos;s MLIR really?
MLIR (Multi-Level Intermediate Representation) is a compiler infrastructure for building IRs. It provides the core machinery like types, operations, optimization passes, and lets you define &quot;dialects&quot; on top of it. StableHLO is one such dialect. When you see &quot;MLIR&quot; in the ZML context, it usually means &quot;the framework that StableHLO is built on,&quot; not a separate layer.

&lt;h2&gt;StableHLO&lt;/h2&gt;
&lt;p&gt;One layer under the ZML graph, we have StableHLO (HLO standing for High-Level Operations). StableHLO is an intermediate representation for ML computations. It&apos;s an MLIR dialect, which is why &quot;StableHLO&quot; and &quot;MLIR&quot; are sometimes used interchangeably. The common confusion is to think StableHLO &lt;em&gt;is&lt;/em&gt; MLIR, when really StableHLO is a vocabulary of operations defined &lt;em&gt;within&lt;/em&gt; the MLIR framework.&lt;/p&gt;
&lt;p&gt;The core value proposition is that StableHLO defines a standard, portable set of operations, like &lt;code&gt;stablehlo.add&lt;/code&gt;, &lt;code&gt;stablehlo.dot_general&lt;/code&gt;, &lt;code&gt;stablehlo.broadcast_in_dim&lt;/code&gt;, etc. Plenty of frameworks already use StableHLO, like JAX. There&apos;s even a PyTorch XLA backend that works similarly. So what ZML is doing isn&apos;t entirely new; the difference is that ZML makes this the default and only path.&lt;/p&gt;
&lt;p&gt;The StableHLO of our elementwise multiplication would look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module {
  func.func @main(%0: tensor&amp;lt;1x4xf32&amp;gt;, %1: tensor&amp;lt;1x4xf32&amp;gt;) -&amp;gt; tensor&amp;lt;1x4xf32&amp;gt; {
    %2 = stablehlo.multiply %0, %1 : tensor&amp;lt;1x4xf32&amp;gt;
    return %2 : tensor&amp;lt;1x4xf32&amp;gt;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not the nicest thing to read, but still pretty understandable. &lt;code&gt;tensor&amp;lt;1x4xf32&amp;gt;&lt;/code&gt; is a 1x4 tensor of 32-bit floats, assigned to SSA value &lt;code&gt;%0&lt;/code&gt;. This would be our weights. And the other 1x4 tensor with id &lt;code&gt;%1&lt;/code&gt; is &lt;code&gt;x&lt;/code&gt;. The tensor with the id &lt;code&gt;%2&lt;/code&gt; is whatever comes out of the instruction &lt;code&gt;stablehlo.multiply&lt;/code&gt;, where &lt;code&gt;%0&lt;/code&gt; and &lt;code&gt;%1&lt;/code&gt; are arguments to that function. And we can infer that &lt;code&gt;%2&lt;/code&gt; has the shape 1x4 as well.&lt;/p&gt;
&lt;p&gt;We&apos;re building up a compute graph of the entire thing, all without actual data. As long as we know the shapes, this works.&lt;/p&gt;

Caveat
StableHLO does actually support dynamic shapes, e.g., an unknown batch size `tensor`, but this is exactly what we&apos;d like to avoid in order to compile ahead of time. ZML doesn&apos;t support it either and we&apos;ll skip it for our educational ML framework as well.

&lt;p&gt;You might have noticed that our ZML code didn&apos;t specify any shapes for the &lt;code&gt;weight&lt;/code&gt; tensor. Only &lt;code&gt;x&lt;/code&gt; was defined as &lt;code&gt;zml.Tensor.init(.{1, 4}, .f32);&lt;/code&gt;. So how did it become 1x4 in this case? We&apos;ll address this later when we build our own framework, but to peek into the future a bit: ZML (and our future framework) actually looks at the header of a &lt;code&gt;.safetensor&lt;/code&gt; file to determine the final shapes of the &lt;code&gt;weight&lt;/code&gt; tensor. All &lt;code&gt;.safetensor&lt;/code&gt; files have a JSON part at the start of the file containing each tensor&apos;s name, dtype, shape, and byte offsets, all before the actual data blob. Our struct tells us what tensors our model consists of, and we map this to a &lt;code&gt;.safetensor&lt;/code&gt; file which then tells us the final exact shape of everything.&lt;/p&gt;
&lt;p&gt;This means that our modelling code is more flexible: the same model definition works for a 7B and 13B model. But we can still trace and compile the entire graph by just reading a few kilobytes of data, without loading the model weights.&lt;/p&gt;
&lt;h2&gt;PJRT&lt;/h2&gt;
&lt;p&gt;The last missing piece is how this actually gets executed on a machine. StableHLO is just an IR, it can&apos;t run by itself. The missing piece is PJRT. PJRT is honestly a big beast to tackle, and I&apos;ve just scratched the surface, but the key part to understand is that PJRT defines a compilation entry point and different plugins (for GPU, CPU, TPU, etc.) do the heavy lifting.&lt;/p&gt;
&lt;p&gt;In simple terms: we hand the StableHLO text to PJRT, and if we target CPU, the plugin goes through LLVM and produces native machine code. This is how our ZML defined multiply becomes something that actually runs on a computer.&lt;/p&gt;

More on GPU compilation targets
&lt;p&gt;If you target an Nvidia GPU, you&apos;ll use the XLA:GPU backend, which produces PTX (a GPU intermediate format) plus PJRT metadata. At runtime, the CUDA driver JIT-compiles the PTX to SASS (native GPU machine code) for the specific GPU architecture.&lt;/p&gt;
&lt;p&gt;It&apos;s possible that the XLA:GPU backend emits SASS directly, targeting a specific architecture like sm_80 or sm_90. In that case there&apos;s no JIT step.&lt;/p&gt;

&lt;p&gt;But it&apos;s not a regular binary you can execute directly. Most of the time it isn&apos;t even on disk; it&apos;s kept in memory. You can save it as a PJRT executable, but unlike a regular ELF binary (or Mach-O on macOS), it doesn&apos;t have an entry point. It also contains metadata related to the PJRT runtime.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Here&apos;s the thing that took me a while to internalize: when you build something with ZML, there are two programs and two compilations.&lt;/strong&gt; The Zig program is the &lt;em&gt;host&lt;/em&gt;; the StableHLO graph is the &lt;em&gt;device program&lt;/em&gt;. They have separate compilation pipelines and separate runtimes. The neural network, expressed in ZML tensors and Zig structs, gets compiled through StableHLO and PJRT into a PJRT executable. But that PJRT executable can&apos;t be run as a standalone process; the rest of your Zig application takes that executable and runs it with the correct inputs and outputs. And the Zig program is compiled as Zig normally is.&lt;/p&gt;
&lt;p&gt;When PJRT takes over and actually executes the graph, that&apos;s when real data gets attached to buffers and kernels get dispatched. Everything up until this point has been purely about shapes, graphs, and symbolic values.&lt;/p&gt;
&lt;p&gt;Coming from the PyTorch world, it wasn&apos;t always clear to me where the line between these two goes, and when you cross it. I&apos;ll try to make that line very explicit when we start building.&lt;/p&gt;
&lt;p&gt;A nice consequence of this separation: compiling the graph is something you can do in advance and save as a PJRT executable, then load it from disk when you start your program. You don&apos;t always need to do this; ZML does a clever trick by loading the weights and compiling the graph concurrently, so as long as weight loading is the bottleneck, compilation is essentially free.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;When I got this far in my understanding, the inevitable question entered my mind: it can&apos;t be too hard to write a small toy ML framework that produces StableHLO, right? Turns out it&apos;s not &lt;em&gt;that&lt;/em&gt; hard. And that&apos;s exactly what we&apos;ll build together in Part 2.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Part 2: Building&lt;/h1&gt;
&lt;p&gt;In this part we&apos;ll build a small ML framework that lets us:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build a high-level graph describing some computation&lt;/li&gt;
&lt;li&gt;Turn that graph into valid StableHLO&lt;/li&gt;
&lt;li&gt;Hand that StableHLO to PJRT and execute the graph on CPU&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To keep the scope small, we&apos;ll focus only on a simple &lt;code&gt;y = x·w + b&lt;/code&gt; layer. First we&apos;ll build the graph and emit StableHLO. The API will be ugly and nothing like PyTorch or ZML. Then we&apos;ll wire up PJRT to actually execute it. In Part 4 of the blog, we&apos;ll clean up the API to something that feels more like a real framework.&lt;/p&gt;
&lt;p&gt;If we jumped straight to the nice API, we&apos;d miss the learning. There&apos;s a fair amount of quality-of-life work that frameworks like PyTorch and ZML do that, while satisfying to build, can obscure the underlying concepts.&lt;/p&gt;
&lt;p&gt;All the code is available at &lt;a href=&quot;https://github.com/ErikKaum/fusebox/tree/first-version&quot; target=&quot;_blank&quot;&gt;github.com/ErikKaum/fusebox&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Building tensors&lt;/h2&gt;
&lt;p&gt;I&apos;m calling this library fusebox: &quot;box&quot; because it&apos;s small, &quot;fuse&quot; because there&apos;s still some fire in it.&lt;/p&gt;
&lt;p&gt;We&apos;ll start with four small modules. These are simple, but having good intuition around them matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/dtype.rs&lt;/code&gt;: just &lt;code&gt;F32&lt;/code&gt; for now, with a method to print the MLIR string (&lt;code&gt;&quot;f32&quot;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/shape.rs&lt;/code&gt;: &lt;code&gt;Shape { dims: Vec&amp;lt;i64&amp;gt;, dtype: DType }&lt;/code&gt; dimensions plus dtype, with a method to produce the MLIR tensor type string (e.g. &lt;code&gt;tensor&amp;lt;2x4xf32&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/value.rs&lt;/code&gt;: &lt;code&gt;ValueId(u32)&lt;/code&gt; the SSA id in the graph. Instead of storing it as a string like &lt;code&gt;%5&lt;/code&gt; everywhere, we store the integer and only render it when printing&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/tensor.rs&lt;/code&gt;: &lt;code&gt;Tensor { shape: Shape, value: ValueId }&lt;/code&gt; the core building block that combines all of the above&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The tensor is essentially a symbolic handle. When you do something like &lt;code&gt;matmul(x, w)&lt;/code&gt;, the builder (which we&apos;ll build next) will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;allocate a fresh &lt;code&gt;ValueId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;append an instruction saying &quot;this new value id is the result of DotGeneral(lhs=x.value, rhs=w.value, ...)&quot;&lt;/li&gt;
&lt;li&gt;and return a new &lt;code&gt;Tensor&lt;/code&gt; pointing at that id&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which is exactly the same process that we had previously, in the elementwise multiplication example. No data, just a reference to a result in the computation graph.&lt;/p&gt;
&lt;p&gt;I won&apos;t go through exactly all of the code since it would be way too verbose, but I&apos;ll show code examples with the aim that it&apos;ll improve your intuition around this.&lt;/p&gt;
&lt;p&gt;To reiterate: a tensor has a shape, in this case 1x4, a datatype, and the SSA Value attached to it. This is how our high-level Rust struct maps very directly to StableHLO.&lt;/p&gt;
&lt;p&gt;A tensor &lt;code&gt;t&lt;/code&gt; like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let t = Tensor {
    shape: Shape {
        dims: vec![1, 4],
        dtype: DType::F32,   // → &quot;f32&quot;
    },
    value: ValueId(3),       // → &quot;%3&quot; in the StableHLO output
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Would be represented like this in StableHLO:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%3 : tensor&amp;lt;1x4xf32&amp;gt;
 ^         ^  ^ ^
 |         |  | └── DType::F32
 |         dims: [1, 4]
 ValueId(3)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Building graphs&lt;/h2&gt;
&lt;p&gt;Now we&apos;re ready to start connecting tensors together into a graph.&lt;/p&gt;
&lt;p&gt;First, we&apos;ll make our own intermediate representation (IR) in Rust, which then gets converted into StableHLO. &quot;Another IR?!&quot;, I hear you say. Bear with me. ZML, for example, doesn&apos;t have its own IR; its tensors get directly converted into StableHLO, and that&apos;s probably what you&apos;d do for a real framework. But for learning purposes, having a thin Rust representation of the graph before converting it to StableHLO lets us separate the logic of &lt;em&gt;building the graph&lt;/em&gt; from the concerns of &lt;em&gt;StableHLO syntax&lt;/em&gt;. And I really don&apos;t want to write a blog about StableHLO syntax.&lt;/p&gt;
&lt;p&gt;So we&apos;ll have three new modules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/ir.rs&lt;/code&gt;: defines functions, parameters, and instructions&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/print_mlir.rs&lt;/code&gt;: takes our internal IR and prints it as StableHLO&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/builder.rs&lt;/code&gt;: defines a &lt;code&gt;FuncBuilder&lt;/code&gt; that ties tensors together into a graph&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&apos;s walk through this carefully, since there&apos;s a handful of moving parts here.&lt;/p&gt;
&lt;p&gt;At the top level we have a &lt;strong&gt;module&lt;/strong&gt;, which contains a list of functions. Think of a module as the entry point, in our case, the top-level &lt;code&gt;forward&lt;/code&gt; of a neural network.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;function&lt;/strong&gt; has three parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Parameters&lt;/strong&gt; — the inputs to the function, each &lt;em&gt;with&lt;/em&gt; an SSA value&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instructions&lt;/strong&gt; — the operations inside the function, each &lt;em&gt;producing&lt;/em&gt; an SSA value&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A return value&lt;/strong&gt; — an SSA value that becomes the output&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To give an overview, these are the main structs &amp;amp; enums we&apos;re working with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub struct Module {
    pub functions: Vec&amp;lt;Function&amp;gt;,
}

pub struct Function {
    pub name: String,
    pub params: Vec&amp;lt;Param&amp;gt;,
    pub insts: Vec&amp;lt;Stmt&amp;gt;,
    pub ret: Option&amp;lt;ValueId&amp;gt;,
}

pub struct Param {
    pub name: String,
    pub shape: Shape,
    pub value: ValueId,
}

pub struct Stmt {
    pub result: ValueId,
    pub inst: Inst,
}

pub enum Inst {
    DotGeneral(DotGeneral),
    BroadcastInDim(BroadcastInDim),
    Add(Add),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So each &lt;code&gt;Inst&lt;/code&gt; variant (like &lt;code&gt;DotGeneral&lt;/code&gt;, &lt;code&gt;Add&lt;/code&gt; and &lt;code&gt;BroadcastInDim&lt;/code&gt;) carries the &lt;code&gt;ValueIds&lt;/code&gt; of its inputs plus the output shape. If we look closer at the &lt;code&gt;Add&lt;/code&gt; instruction:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[derive(Debug, Clone)]
pub struct Add {
    pub lhs: ValueId,
    pub rhs: ValueId,
    pub out: Shape,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And let&apos;s say our left-hand side has the value id &lt;code&gt;0&lt;/code&gt; and the rhs has id &lt;code&gt;1&lt;/code&gt;. Then we could look at the shape and dtype of tensors &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;1&lt;/code&gt; respectively, and produce the following StableHLO:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;stablehlo.add&quot;(%0, %1) : (tensor&amp;lt;1x4xf32&amp;gt;, tensor&amp;lt;1x4xf32&amp;gt;) -&amp;gt; tensor&amp;lt;1x4xf32&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is how we essentially keep a mapping of each StableHLO instruction, and have a corresponding Rust struct for each one of them. Later we&apos;ll also see how we explicitly set the output shape and dtype.&lt;/p&gt;
&lt;p&gt;If this doesn&apos;t fully make sense yet, I&apos;d encourage reading it once more. Having a solid understanding of the IR made everything else click for me.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/ErikKaum/fusebox/blob/first-version/src/ir.rs&quot; target=&quot;_blank&quot;&gt;full IR definitions&lt;/a&gt; are in the repo; they&apos;re straightforward once you see the pattern.&lt;/p&gt;
&lt;h2&gt;Building builders&lt;/h2&gt;
&lt;p&gt;Now the builder. We&apos;ll create a struct called the &lt;code&gt;FuncBuilder&lt;/code&gt;. Each instruction we defined in the IR gets a corresponding method on the &lt;code&gt;FuncBuilder&lt;/code&gt;: &lt;code&gt;builder.add(...)&lt;/code&gt;, &lt;code&gt;builder.dot_general(...)&lt;/code&gt;, &lt;code&gt;builder.broadcast_in_dim(...)&lt;/code&gt;, and so on.&lt;/p&gt;
&lt;p&gt;But before those, let&apos;s look at the &lt;code&gt;fresh()&lt;/code&gt; method. This will be the function that gives fresh &lt;code&gt;ValueIds&lt;/code&gt;, and essentially the way we do bookkeeping of the ids. It&apos;s pretty simple, just incrementing a counter. But note that this also makes the &lt;code&gt;FuncBuilder&lt;/code&gt; stateful. If we change the order from &lt;code&gt;add(x, y)&lt;/code&gt; to &lt;code&gt;add(y, x)&lt;/code&gt;, the result is the same since addition is commutative, and the result tensor gets the same &lt;code&gt;ValueId&lt;/code&gt; either way. But &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; themselves will get swapped &lt;code&gt;ValueIds&lt;/code&gt;, whichever is added to the graph first gets the lower id. That means &lt;code&gt;ValueIds&lt;/code&gt; aren&apos;t a stable way to reference a tensor: the same logical tensor can end up with different ids depending on construction order. It&apos;s fine for now, but this will come up later as a real issue when we want to produce a stable ABI on top of ValueIds.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub struct FuncBuilder {
    func: Function,
    next_id: u32,
}

impl FuncBuilder {
    
    fn fresh(&amp;amp;mut self) -&amp;gt; ValueId {
        let id = self.next_id;
        self.next_id += 1;
        ValueId(id)
    } 
    
    pub fn matmul_2d(&amp;amp;mut self, x: &amp;amp;Tensor, w: &amp;amp;Tensor) -&amp;gt; Result&amp;lt;Tensor, Error&amp;gt; {
        ...
    }

    pub fn broadcast_bias_1d(&amp;amp;mut self, b: &amp;amp;Tensor, batch: i64) -&amp;gt; Result&amp;lt;Tensor, Error&amp;gt; {
        ...
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we dive deeper in how the &lt;code&gt;add()&lt;/code&gt; is actually implemented, we&apos;ll find that most of the ceremony is validating shapes and dtypes. Which I&apos;ve omitted in this blog. But the core pattern is always the same:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub fn add(&amp;amp;mut self, a: &amp;amp;Tensor, b: &amp;amp;Tensor) -&amp;gt; Result&amp;lt;Tensor, Error&amp;gt; {
    // ... shape/dtype validation ...

    let out_shape = a.shape.clone(); // how we manually determine output shapes
    let result = self.fresh();  // allocate a new ValueId (just a counter)

    self.func.insts.push(Stmt {
        result,
        inst: Inst::Add(Add {
            lhs: a.value,
            rhs: b.value,
            out: out_shape.clone(),
        }),
    });

    Ok(Tensor::new(out_shape, result))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We call &lt;code&gt;self.fresh()&lt;/code&gt;, which gives us a new id. Then we create a new statement: this value id is the result of the &lt;code&gt;add&lt;/code&gt; instruction on the two inputs. We append it to the function&apos;s instruction list and return a new &lt;code&gt;Tensor&lt;/code&gt; pointing at the result.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;matmul_2d&lt;/code&gt; and &lt;code&gt;broadcast_bias_1d&lt;/code&gt; methods follow the exact same pattern: validate shapes, allocate a fresh ValueId, push a statement, return a new Tensor.
To keep things simple, I&apos;ve kept the methods specific to a certain dimension, so &lt;code&gt;matmul_2d&lt;/code&gt; instead of just &lt;code&gt;matmul&lt;/code&gt;, we&apos;ll relax this later. Here you&apos;ll find the &lt;a href=&quot;https://github.com/ErikKaum/fusebox/blob/first-version/src/builder.rs&quot; target=&quot;_blank&quot;&gt;full builder code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If this doesn&apos;t 100% click yet, this example should help. Here&apos;s the full &lt;code&gt;y = x·w + b&lt;/code&gt; layer built with the current API of our framework:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let mut f = FuncBuilder::new(&quot;main&quot;);

let x = f.param(&quot;x&quot;, Shape::new(vec![2, 4], DType::F32));   // -&amp;gt; %0
let w = f.param(&quot;w&quot;, Shape::new(vec![4, 3], DType::F32));   // -&amp;gt; %1
let b = f.param(&quot;b&quot;, Shape::new(vec![3], DType::F32));      // -&amp;gt; %2

let y = f.matmul_2d(&amp;amp;x, &amp;amp;w)?;                               // -&amp;gt; %3
let bb = f.broadcast_bias_1d(&amp;amp;b, 2)?;                       // -&amp;gt; %4
let out = f.add(&amp;amp;y, &amp;amp;bb)?;                                  // -&amp;gt; %5

f.ret(&amp;amp;out);

let func = f.finish();
let module = Module { functions: vec![func] };
println!(&quot;{}&quot;, print_module(&amp;amp;module));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We instantiate the &lt;code&gt;FuncBuilder&lt;/code&gt;, add three parameters (x, w, b), chain a matmul, a broadcast, and an add, set the return, and finish. Essentially the same thing as in this graph:&lt;/p&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/zml/2.png&quot; alt=&quot;graph&quot; /&gt;
&lt;/div&gt;
&lt;p&gt;Each &lt;code&gt;f.param()&lt;/code&gt; call does two things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it allocates the next ValueId to each tensor; this is how &lt;code&gt;x&lt;/code&gt; becomes &lt;code&gt;%0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;it registers the tensor as a function input&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So our three params become the function signature @main(%0: ..., %1: ..., %2: ...), while the intermediate tensors (&lt;code&gt;%3&lt;/code&gt;, &lt;code&gt;%4&lt;/code&gt;) are produced by operations in the function body. The final result &lt;code&gt;%5&lt;/code&gt; is what gets returned. Once we print it, we get StableHLO (truncated for readability):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module { 
    func.func @main(%0: tensor&amp;lt;2x4xf32&amp;gt;, %1: tensor&amp;lt;4x3xf32&amp;gt;, %2: tensor&amp;lt;3xf32&amp;gt;) -&amp;gt; tensor&amp;lt;2x3xf32&amp;gt; {
        %3 = stablehlo.dot_general ...
        %4 = stablehlo.broadcast_in_dim ...
        %5 = stablehlo.add %3, %4 : tensor&amp;lt;2x3xf32&amp;gt;
        return %5 : tensor&amp;lt;2x3xf32&amp;gt; 
    } 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can save this text as a &lt;code&gt;.mlir&lt;/code&gt; file and use a &lt;a href=&quot;https://github.com/openxla/stablehlo/tree/main/stablehlo/tools&quot;&gt;tool from the StableHLO repo&lt;/a&gt; to verify that the output is valid. Running &lt;code&gt;stablehlo-opt forward.mlir&lt;/code&gt; will catch any syntax or type errors.&lt;/p&gt;
&lt;p&gt;So far we&apos;ve:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Created a high-level way of representing tensors with shapes and dtypes&lt;/li&gt;
&lt;li&gt;Built a small IR that can represent basic StableHLO operations&lt;/li&gt;
&lt;li&gt;Tied them together with a function builder that creates a computation graph&lt;/li&gt;
&lt;li&gt;Printed the graph as StableHLO&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you think the API feels weird compared to PyTorch or ZML, you&apos;re not alone. We&apos;re calling methods on a builder rather than doing &lt;code&gt;tensor.add()&lt;/code&gt;. In Part 4 we&apos;ll clean this up and start building layers on top of the &lt;code&gt;FuncBuilder&lt;/code&gt;, which will make the API feel more like a modern ML framework.&lt;/p&gt;
&lt;p&gt;But this actually works! It produces real StableHLO, which means that we can move to the next part of the pipeline: compile it with PJRT and actually run it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Part 3: Running&lt;/h1&gt;
&lt;p&gt;We&apos;re finally getting close to producing something. After all this plumbing, we&apos;ve reached the step of actually executing a computation graph. If you&apos;ve felt so far that we&apos;re not doing much (&quot;it&apos;s just a StableHLO wrapper, right?&quot;), you&apos;ll start to feel it now.&lt;/p&gt;
&lt;p&gt;In plain terms, we now have a program that produces StableHLO (basically a text file). We&apos;ll hand this text file to another program, wire up the correct inputs, compile it, and run it. To really drive home the &quot;two programs&quot; point, I first wrote this runner in Go. There&apos;s no need for the graph-builder and the runner to be in the same language, and using a different one makes the separation palpable.&lt;/p&gt;
&lt;h2&gt;Quick and dirty in Go&lt;/h2&gt;
&lt;p&gt;We&apos;ll use a Go package called &lt;code&gt;go-xla&lt;/code&gt;. The painful part of working with PJRT directly is keeping track of plugin versions (which you don&apos;t have to do in ZML); &lt;code&gt;go-xla&lt;/code&gt; handles this with an &lt;code&gt;installer.AutoInstall()&lt;/code&gt; function that auto-downloads the correct precompiled PJRT plugin for your platform.&lt;/p&gt;
&lt;p&gt;The flow has essentially these three steps: 1) read the MLIR file, 2) compile it, and 3) execute it with input buffers.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
    // 1. Auto-download PJRT plugin
    installer.AutoInstall(&quot;&quot;, true, installer.Normal)

    // 2. Read StableHLO and compile it
    mlirBytes, _ := os.ReadFile(&quot;forward.mlir&quot;)
    plugin, _ := pjrt.GetPlugin(&quot;cpu&quot;)
    client, _ := plugin.NewClient(nil)
    exec, _ := client.Compile().WithStableHLO(mlirBytes).Done()

    // 3. Create input buffers with REAL DATA and execute
    x := []float32{1, 2, 3, 4, 5, 6, 7, 8}
    w := []float32{1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1}
    b := []float32{10, 20, 30}

    bx, _ := pjrt.ArrayToBuffer(client, x, 2, 4)
    bw, _ := pjrt.ArrayToBuffer(client, w, 4, 3)
    bb, _ := pjrt.ArrayToBuffer(client, b, 3)

    outs, _ := exec.Execute(bx, bw, bb).Done()
    outFlat, outDims, _ := pjrt.BufferToArray[float32](outs[0])

    fmt.Printf(&quot;output dims=%v\n&quot;, outDims)
    fmt.Printf(&quot;output flat=%v\n&quot;, outFlat)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the key moment: &lt;strong&gt;only now, at &lt;code&gt;ArrayToBuffer&lt;/code&gt;, do we pass real data&lt;/strong&gt;. Everything before this was purely symbolic. The full Go code with proper error handling is in the &lt;a href=&quot;https://github.com/ErikKaum/fusebox/blob/first-version/runner/main.go&quot; target=&quot;_blank&quot;&gt;fusebox repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And once you run this with &lt;code&gt;go run main.go&lt;/code&gt;, you&apos;ll get:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;output dims=[2 3]
output flat=[15 26 37 23 34 45]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;ve come this far, you should congratulate yourself. You&apos;ve understood the entire pipeline from start to finish. Everything from here onwards enhances and polishes this pipeline, but conceptually we won&apos;t change anything fundamental. This is it. But don&apos;t be fooled, there&apos;s still tons of work to do before we can call this a toy ML framework.&lt;/p&gt;
&lt;h2&gt;Moving the runner to Rust&lt;/h2&gt;
&lt;p&gt;The obvious shortcoming is that we can&apos;t handwrite input arrays in Go for every StableHLO program. As a silly solution, we could create a JSON file that would describe the inputs with shapes and dtypes. We&apos;d have to generate this in the Rust code, and hand it over to the Go runner. Something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;entry&quot;: &quot;main&quot;,
  &quot;inputs&quot;: [
    {&quot;name&quot;: &quot;x&quot;, &quot;dtype&quot;: &quot;f32&quot;, &quot;shape&quot;: [2,4]},
    {&quot;name&quot;: &quot;w&quot;, &quot;dtype&quot;: &quot;f32&quot;, &quot;shape&quot;: [4,3]},
    {&quot;name&quot;: &quot;b&quot;, &quot;dtype&quot;: &quot;f32&quot;, &quot;shape&quot;: [3]}
  ],
  &quot;outputs&quot;: [
    {&quot;dtype&quot;: &quot;f32&quot;, &quot;shape&quot;: [2,3]}
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But wouldn&apos;t it be nice if the runner could infer the shapes and dtypes from the builder&apos;s params. Rather than having to type out things twice: once for building the graph and a second time for passing real data to the graph.&lt;/p&gt;
&lt;p&gt;We&apos;ll do this, but not quite yet. First we need to have a runner in Rust. We&apos;ll use the &lt;code&gt;pjrt&lt;/code&gt; crate, which doesn&apos;t have an autoinstaller like &lt;code&gt;go-xla&lt;/code&gt;. Fortunately, ZML maintains a pre-compiled repo of the PJRT plugins. It took some trial and error to find which plugin version corresponds to the Rust bindings. Version 0.2.2 at least seems to match. Again, you don&apos;t have to think about this with ZML.&lt;/p&gt;
&lt;p&gt;When moving the runner to Rust, we have an API that looks like this. We still set up host buffers manually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;fn main() -&amp;gt; Result&amp;lt;(), String&amp;gt; {
    let mlir = build_your_stablehlo_mlir_string();

    let runner = PjrtCpuRunner::from_mlir_text(&amp;amp;mlir, default_cpu_plugin_path())?;

    let x = HostTensorF32::new(vec![2, 4], vec![1.,2.,3.,4.,  5.,6.,7.,8.])?;
    let w = HostTensorF32::new(vec![4, 3], vec![1.,2.,3., 4.,5.,6., 7.,8.,9., 10.,11.,12.])?;
    let b = HostTensorF32::new(vec![3], vec![1., 2., 3.])?;

    let y = runner.run_f32(vec![x, w, b])?;
    println!(&quot;output dims={:?}&quot;, y.dims);
    println!(&quot;output flat={:?}&quot;, y.data);

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ErikKaum/fusebox/blob/first-version/src/pjrt_runtime.rs&quot; target=&quot;_blank&quot;&gt;Link to the full code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At this point we have a complete pipeline entirely in Rust: define the graph, emit StableHLO, compile via PJRT, execute, and get results back. The API is rough, but it works.&lt;/p&gt;
&lt;p&gt;The next part is about making it feel like a real framework.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Part 4: Cleaning&lt;/h1&gt;
&lt;p&gt;So far we&apos;ve built a framework that takes high-level tensors, builds a computation graph in StableHLO, compiles it via PJRT, and executes it on CPU. It works, but the API leaves a lot to be desired. We&apos;ve been optimizing for learning instead of ergonomics, and now it&apos;s time to make this feel like a real framework.&lt;/p&gt;
&lt;p&gt;If you&apos;ve made it this far, you already have a solid understanding of the stack that ZML builds upon: which is the main goal of this blog. Feel free to call it a day. But if you&apos;re curious about the long tail of details that make a framework usable rather than just correct, read on. Nothing here changes the fundamentals, but these are the edges that separate a StableHLO wrapper from something you&apos;d actually want to use.&lt;/p&gt;
&lt;h2&gt;From graph plumbing to model code&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;FuncBuilder&lt;/code&gt; approach felt like building &quot;from the outside.&quot; We pushed parameters into a builder, called graph-building helpers, and printed a StableHLO function. This was great for learning, but the code that describes a model doesn&apos;t look like a model: it looks like graph plumbing.&lt;/p&gt;
&lt;p&gt;The shift we want to make is to go from manually constructing a graph to automatically tracing a forward pass. Enter the &lt;code&gt;TraceCx&lt;/code&gt; (tracing context). You write a forward pass once, and it runs in a mode where tensor operations don&apos;t compute; instead, they&apos;re recorded. Similar to what&apos;s done in ZML and JAX.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub struct TraceCx {
    builder: Rc&amp;lt;RefCell&amp;lt;FuncBuilder&amp;gt;&amp;gt;,
    prefix: String,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;TraceCx&lt;/code&gt; wraps our &lt;code&gt;FuncBuilder&lt;/code&gt; and adds two key things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;hierarchical name scoping (the &lt;code&gt;prefix&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;a distinction between input parameters and weight parameters&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We&apos;ll see why both of these matter shortly.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;TraceCx&lt;/code&gt;, a linear layer becomes a struct with a &lt;code&gt;forward&lt;/code&gt; method. This looks a lot more like the modelling code we&apos;re familiar with from PyTorch.
The big difference is that we&apos;re still manually writing the &lt;code&gt;trace_init()&lt;/code&gt; method.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub struct Linear {
    pub w: Tensor,
    pub b: Option&amp;lt;Tensor&amp;gt;,
}

impl Linear {
    pub fn trace_init(cx: &amp;amp;mut TraceCx, name: &amp;amp;str, in_dim: i64, out_dim: i64, bias: bool) -&amp;gt; Self {
        let _s = cx.push_scope(name);
        let w = cx.weight(&quot;w&quot;, Shape::new(vec![in_dim, out_dim], DType::F32));
        let b = if bias {
            Some(cx.weight(&quot;b&quot;, Shape::new(vec![out_dim], DType::F32)))
        } else {
            None
        };
        cx.pop_scope(_s);
        Self { w, b }
    }

    pub fn forward(&amp;amp;self, cx: &amp;amp;mut TraceCx, x: &amp;amp;Tensor) -&amp;gt; Result&amp;lt;Tensor, Error&amp;gt; {
        let y = cx.matmul_2d(x, &amp;amp;self.w)?;
        if let Some(b) = &amp;amp;self.b {
            let batch = x.shape.dim(0);
            let bb = cx.broadcast_bias_1d(b, batch)?;
            cx.add(&amp;amp;y, &amp;amp;bb)
        } else {
            Ok(y)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&apos;d use it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let mut cx = TraceCx::new(&quot;main&quot;);

let x = cx.input(&quot;x&quot;, Shape::new(vec![2, 4], DType::F32));
let proj = Linear::trace_init(&amp;amp;mut cx, &quot;proj&quot;, 4, 3, true);
let y = proj.forward(&amp;amp;mut cx, &amp;amp;x)?;

cx.set_ret(&amp;amp;y);
let func = cx.finish();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In practice, &lt;code&gt;TraceCx&lt;/code&gt; is a nicer facade over the same essentials the builder had: it owns the function-under-construction, hands out ValueIds, provides ops, and produces StableHLO. The important shift is &lt;em&gt;where&lt;/em&gt; the model logic lives. A module is a struct with weight fields; &lt;code&gt;forward&lt;/code&gt; is a method that composes ops; tracing works by calling &lt;code&gt;forward&lt;/code&gt; once with symbolic tensors.&lt;/p&gt;
&lt;p&gt;That might just look like semantic changes, but it&apos;s the first step from &quot;graph construction code&quot; into &quot;model code.&quot; And later, we can extract this pattern into a proc-macro, which is really when it starts to feel like magic.&lt;/p&gt;
&lt;h2&gt;Signatures: the bridge to PJRT&lt;/h2&gt;
&lt;p&gt;Now that we&apos;ve cleaned up how the graph gets constructed by having a &lt;code&gt;TraceCx&lt;/code&gt;, let&apos;s look at the next step.&lt;/p&gt;
&lt;p&gt;Remember, the last line from the previous example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let func = cx.finish();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;func&lt;/code&gt; holds the StableHLO text that we want to pass to the PJRT runtime.&lt;/p&gt;
&lt;p&gt;But we&apos;re still manually creating &lt;code&gt;HostTensorF32&lt;/code&gt; values that match the graph. Let&apos;s fix that. Instead, we want the &lt;code&gt;TraceCx&lt;/code&gt; to produce a structured signature that the runtime can use directly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub struct ParamSpec {
    pub name: String,
    pub shape: Shape,
    pub value: ValueId,
    pub kind: ParamKind,
}

pub struct Signature {
    params: Vec&amp;lt;ParamSpec&amp;gt;,
    by_name: HashMap&amp;lt;String, usize&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Signature&lt;/code&gt; is extracted from the traced parameters: name, shape, dtype, and whether it&apos;s an input or a weight. It becomes the single source of truth for runtime binding. The runner can validate inputs, pack them in the correct argument order, and later separate which parameters are the model weights (which need to be bound once) from which parameters are the inputs (bound per request).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let runner = PjrtCpuRunner::from_function(&amp;amp;func, default_cpu_plugin_path())?;

let mut ins = runner.inputs();
ins.set(&quot;x&quot;, vec![1., 2., 3., 4., 5., 6., 7., 8.])?;
ins.set(&quot;proj/w&quot;, vec![1., 0., 0., 0., 1., 0., 0., 0., 1., 1., 1., 1.])?;
ins.set(&quot;proj/b&quot;, vec![10., 20., 30.])?;

let y = runner.run(ins)?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Concretely, what happens is that we pass the &lt;code&gt;&amp;amp;func&lt;/code&gt; into the &lt;code&gt;PjrtCpuRunner::from_function(...)&lt;/code&gt; and then set the vectors as the runner inputs through &lt;code&gt;ins = runner.inputs()&lt;/code&gt; and &lt;code&gt;ins.set()&lt;/code&gt;. This is an improvement, but we&apos;re still not reading &lt;code&gt;proj/w&lt;/code&gt; and &lt;code&gt;proj/b&lt;/code&gt; from a safetensor file. And the inputs &lt;code&gt;x&lt;/code&gt; are still bound to the runner the same way as the weights. But we&apos;re getting there.&lt;/p&gt;
&lt;p&gt;Notice the scoped names: &lt;code&gt;&quot;proj/w&quot;&lt;/code&gt; and &lt;code&gt;&quot;proj/b&quot;&lt;/code&gt; came from the &lt;code&gt;cx.push_scope(&quot;proj&quot;)&lt;/code&gt; call in &lt;code&gt;trace_init()&lt;/code&gt;. We&apos;re starting to see the real difference between the &lt;code&gt;TraceCx&lt;/code&gt; and the old &lt;code&gt;FuncBuilder&lt;/code&gt;: the tracer produces an ABI, not just a graph.&lt;/p&gt;
&lt;h2&gt;Stable names and param kinds&lt;/h2&gt;
&lt;p&gt;Now that the tracer produces an ABI, we need to be more careful about naming. Remember the ValueId instability we discussed in Part 2? The SSA value id is not a stable identifier: add a new layer and all the indices shift. For an ABI, we need names that come from the model structure and match the checkpoint.&lt;/p&gt;
&lt;p&gt;What we already saw earlier with the &lt;code&gt;proj&lt;/code&gt; suffix, is the result of the &lt;code&gt;push_scope&lt;/code&gt;/&lt;code&gt;pop_scope&lt;/code&gt; mechanism, which gives us hierarchical names: a &lt;code&gt;Linear&lt;/code&gt; inside an MLP named &lt;code&gt;&quot;proj&quot;&lt;/code&gt; gets weights named &lt;code&gt;&quot;proj/w&quot;&lt;/code&gt; and &lt;code&gt;&quot;proj/b&quot;&lt;/code&gt;. These names are stable. They don&apos;t change when you reorder operations, and they naturally match the naming conventions in safetensors weight files.&lt;/p&gt;
&lt;p&gt;The practical approach is to align the naming of the struct fields (tensors) with the weight file. Meaning that the ultimate goal is that we can directly align the weights in a safetensor file to the tensors in our model. In a later section I&apos;ll add a rename macro &lt;code&gt;#[module(name = &quot;new_name&quot;)]&lt;/code&gt;, for inevitable mismatches. Which is another one of these quality-of-life things you&apos;d want in a real ML framework. But for now, we assume that the names are the same in the safetensor files as in the struct fields.&lt;/p&gt;
&lt;p&gt;The other important distinction is &lt;code&gt;ParamKind&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub enum ParamKind {
    Input,
    Weight,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the graph&apos;s point of view, all parameters are the same, just nodes. But from a model&apos;s point of view, there&apos;s a clear difference. Inputs are a runtime contract, they change per request. Weights are a checkpoint contract, loaded once. This separation matters because in a real serving system, the hot path is request execution. You bind weights once at startup and rebind inputs per request.&lt;/p&gt;
&lt;p&gt;With this change we can avoid doing everything through the &lt;code&gt;ins.set()&lt;/code&gt; which we did above.&lt;/p&gt;
&lt;h2&gt;Where do weight shapes come from?&lt;/h2&gt;
&lt;p&gt;Tracing requires knowing exact tensor shapes, and so far we&apos;ve supplied those shapes manually, like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let proj = Linear::trace_init(&amp;amp;mut cx, &quot;proj&quot;, 4, 3, true);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where 4 and 3 become the in and out dimensions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let w = cx.weight(&quot;w&quot;, Shape::new(vec![in_dim, out_dim], DType::F32));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Obviously, this is not the way.&lt;/p&gt;
&lt;p&gt;ZML&apos;s answer: read the shapes from the checkpoint file&apos;s metadata, compile the graph from shapes alone, and load the actual weight data separately (or in parallel). Importantly, we can compile the graph from just the shapes and dtypes, we don&apos;t need to load the &lt;strong&gt;entire&lt;/strong&gt; safetensor file into memory for this.&lt;/p&gt;
&lt;p&gt;Safetensors makes this convenient because its file header contains tensor names, dtypes, and shapes as JSON, before any of the actual data. You can read a few kilobytes of metadata from a multi-gigabyte file and know everything you need to compile. In fusebox, this became a &lt;code&gt;ShapeProvider&lt;/code&gt; trait:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub trait ShapeProvider {
    fn shape_of(&amp;amp;self, full_name: &amp;amp;str) -&amp;gt; Result&amp;lt;Option&amp;lt;Shape&amp;gt;, Error&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a &lt;code&gt;SafeTensorShapes&lt;/code&gt; implementation that reads just the header. The model initialization step stops taking explicit &lt;code&gt;(in_dim, out_dim)&lt;/code&gt; arguments and starts taking a shape provider.&lt;/p&gt;
&lt;h2&gt;Ops on tensors&lt;/h2&gt;
&lt;p&gt;Next, the biggest API change. So far all operations are done on the &lt;code&gt;TraceCx&lt;/code&gt;, like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let y = cx.matmul_2d(x, &amp;amp;self.w)?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While this is fine, I just don&apos;t like it. I want to be able to do something closer to &lt;code&gt;x.matmul_2d(&amp;amp;self.w)&lt;/code&gt;. The change is that instead of passing &lt;code&gt;&amp;amp;mut TraceCx&lt;/code&gt; through every forward method, tensors themselves carry a reference to the computation graph:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub struct Tensor {
    pub shape: Shape,
    pub value: ValueId,
    pub(crate) graph: Rc&amp;lt;RefCell&amp;lt;FuncBuilder&amp;gt;&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The graph reference is shared via &lt;code&gt;Rc&amp;lt;RefCell&amp;lt;...&amp;gt;&amp;gt;&lt;/code&gt;. When you do &lt;code&gt;&amp;amp;a + &amp;amp;b&lt;/code&gt;, the &lt;code&gt;Add&lt;/code&gt; impl borrows the graph, appends the instruction, and returns a new &lt;code&gt;Tensor&lt;/code&gt; pointing at the result. All operations become methods on &lt;code&gt;Tensor&lt;/code&gt;, and operator overloads (&lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt;) work for all combinations of owned and borrowed tensors.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;TraceCx&lt;/code&gt; is now only used for declaring inputs and weights. All ops live on &lt;code&gt;Tensor&lt;/code&gt;. Compare the before and after of &lt;code&gt;Linear::forward&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;fn forward(&amp;amp;self, cx: &amp;amp;mut TraceCx, x: &amp;amp;Tensor) -&amp;gt; Result&amp;lt;Tensor, Error&amp;gt; {
    let y = cx.matmul_2d(x, &amp;amp;self.w)?;
    if let Some(b) = &amp;amp;self.b {
        let batch = x.shape.dim(0);
        let bb = cx.broadcast_bias_1d(b, batch)?;
        cx.add(&amp;amp;y, &amp;amp;bb)
    } else {
        Ok(y)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub fn forward(&amp;amp;self, x: &amp;amp;Tensor) -&amp;gt; Result&amp;lt;Tensor, Error&amp;gt; {
    let wt = self.weight.transpose(&amp;amp;[1, 0])?;
    let y = x.matmul(&amp;amp;wt)?;
    match &amp;amp;self.bias {
        Some(b) =&amp;gt; y.add(b),
        None =&amp;gt; Ok(y),
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No more &lt;code&gt;cx&lt;/code&gt; parameter. Also I changed the &lt;code&gt;add&lt;/code&gt; to handle broadcasting automatically, so that removes &lt;code&gt;broadcast_bias_1d&lt;/code&gt;. Now this starts to feel like PyTorch.&lt;/p&gt;
&lt;h2&gt;Macros: removing the last boilerplate&lt;/h2&gt;
&lt;p&gt;Tracing works, ops are clean, but there&apos;s still boilerplate: the &lt;code&gt;trace_init()&lt;/code&gt; method. For every module, you write &quot;allocate w, maybe allocate b&quot; with string names that must match checkpoint keys. We want code generation to handle this. The tracing should happen automatically, and tensor names should be aligned to struct field names.&lt;/p&gt;
&lt;p&gt;I&apos;ve never written a Rust proc-macro before, and that&apos;s still largely true; in this case I just let Opus 4.6 handle it. Learning Rust proc-macros would have been too much of a side quest.&lt;/p&gt;
&lt;p&gt;Anyway. Before, every module had to manually implement this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;impl Linear {
    pub fn trace_init(cx: &amp;amp;mut TraceCx, name: &amp;amp;str, shapes: &amp;amp;dyn ShapeProvider) -&amp;gt; Result&amp;lt;Self, Error&amp;gt; {
        let _s = cx.push_scope(name);
        let w = cx.weight(&quot;weight&quot;, shapes.shape_of(cx.qualify(&quot;weight&quot;))?.unwrap());
        let b = match shapes.shape_of(cx.qualify(&quot;bias&quot;))? {
            Some(s) =&amp;gt; Some(cx.weight(&quot;bias&quot;, s)),
            None =&amp;gt; None,
        };
        cx.pop_scope(_s);
        Ok(Self { w, b })
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After, we just add the derive module macro, and our &lt;code&gt;trace_init()&lt;/code&gt; method gets automatically generated:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[derive(Module)]
pub struct Linear {
    pub weight: Tensor,
    pub bias: Option&amp;lt;Tensor&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In more detail, the &lt;code&gt;#[derive(Module)]&lt;/code&gt; macro generates a &lt;code&gt;trace&lt;/code&gt; (yes I changed the name) method by inspecting struct fields:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Tensor&lt;/code&gt; fields become required weights, looked up via &lt;code&gt;shapes.shape_of()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Option&amp;lt;Tensor&amp;gt;&lt;/code&gt; fields become optional weights, &lt;code&gt;None&lt;/code&gt; if absent from the checkpoint&lt;/li&gt;
&lt;li&gt;Any other type implementing &lt;code&gt;Module&lt;/code&gt; becomes a nested submodule, basically recursive trace in a scoped name&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;/code&gt; where &lt;code&gt;T: Module&lt;/code&gt; auto-discovers layer count by probing the checkpoint for &lt;code&gt;&quot;0/&quot;&lt;/code&gt;, &lt;code&gt;&quot;1/&quot;&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I added two attributes that handle quality-of-life edge cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#[module(name = &quot;gate_proj&quot;)]&lt;/code&gt; renames the checkpoint key, because checkpoints rarely 100% match the preferred Rust field names&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#[module(skip)]&lt;/code&gt; ignores config fields like &lt;code&gt;out_dim: i64&lt;/code&gt; that aren&apos;t submodules&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The macro doesn&apos;t touch &lt;code&gt;forward&lt;/code&gt;, that stays ordinary Rust. It only automates the tedious part of tracing the graph and matching it with a checkpoint.&lt;/p&gt;
&lt;h2&gt;The final API&lt;/h2&gt;
&lt;p&gt;The last step is wrapping the lifecycle into clean types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Device&lt;/code&gt;: wraps a PJRT plugin path. Entry point for &lt;code&gt;compile()&lt;/code&gt; and &lt;code&gt;load()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Checkpoint&lt;/code&gt;: loads a safetensors file once. Exposes both shapes (for tracing) and weight data (for binding)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CompiledModel&lt;/code&gt;: a compiled PJRT executable plus its signature. Can be saved to and loaded from disk&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Session&lt;/code&gt;: a compiled model with pre-bound weights. The thing you call &lt;code&gt;run&lt;/code&gt; on&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&apos;s a small example, a gated MLP loaded from a safetensors checkpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;use fusebox::prelude::*;

#[derive(Module)]
pub struct Mlp {
    pub up: Linear,
    #[module(name = &quot;gate_proj&quot;)]
    pub gate: Linear,
    pub down: Linear,
}

impl Mlp {
    pub fn forward(&amp;amp;self, x: &amp;amp;Tensor) -&amp;gt; Result&amp;lt;Tensor, Error&amp;gt; {
        let gate = self.gate.forward(x)?;
        let gate = gate.silu();
        let up = self.up.forward(x)?;
        let hidden = (&amp;amp;gate * &amp;amp;up)?;
        self.down.forward(&amp;amp;hidden)
    }
}

fn main() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {
    let ckpt = Checkpoint::from_file(&quot;model.safetensors&quot;)?;
    let device = Device::cpu();

    let runner = device.compile(&quot;main&quot;, |cx| {
        let x = cx.input(&quot;x&quot;, Shape::new(vec![1, 8], DType::F32));
        let mlp = Mlp::trace(cx, &quot;proj&quot;, ckpt.shapes())?;
        mlp.forward(&amp;amp;x)
    })?;

    let weights = ckpt.load_weights(runner.signature())?;
    let sess = runner.session(weights);

    let y = sess.run(|inputs| inputs.set_input(&quot;x&quot;, vec![1., 2., 3., 4., 5., 6., 7., 8.]))?;

    println!(&quot;shape: {:?}&quot;, y.shape());
    println!(&quot;data: {:?}&quot;, y.to_f32().unwrap());
    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every piece we built across Parts 2-4 is here, but hidden behind a clean API. &lt;code&gt;Checkpoint::from_file()&lt;/code&gt; reads the safetensors header for shapes. &lt;code&gt;device.compile()&lt;/code&gt; traces the closure, emits StableHLO, and compiles via PJRT. All from shapes alone, no weight data loaded yet. &lt;code&gt;ckpt.load_weights()&lt;/code&gt; validates that weight names match the traced signature and extracts the data. &lt;code&gt;runner.session(weights)&lt;/code&gt; binds weights once. And &lt;code&gt;sess.run&lt;/code&gt; binds only the runtime inputs per request.&lt;/p&gt;
&lt;p&gt;This is similar to the pipeline ZML uses: shapes define types → tracing defines programs → StableHLO is the portable representation → PJRT compiles and runs → good APIs fall out of separating compile time from runtime.&lt;/p&gt;
&lt;p&gt;It&apos;s worth stepping back and acknowledging what fusebox &lt;em&gt;doesn&apos;t&lt;/em&gt; do. We&apos;re making things easy by only supporting CPU. ZML maintains and ships plugins for GPU (CUDA, ROCBlas), TPU, and more. Getting the right plugin version, linking it correctly, and handling platform-specific quirks is a significant engineering effort that ZML handles for you. We&apos;ve also done zero work on performance, even though we get some parts through PJRT automatically.&lt;/p&gt;
&lt;p&gt;There are plenty more things to list here, but this is just to make the point that we&apos;re still far away from making a production-grade framework.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Demo&lt;/h1&gt;
&lt;p&gt;After having started from understanding what a simple &lt;code&gt;add&lt;/code&gt; looks like in StableHLO, it&apos;s extremely satisfying to have a full transformer model implemented in fusebox.&lt;/p&gt;
&lt;p&gt;I expanded the opset to support running a Llama-style LLM. The model definition uses everything we built, &lt;code&gt;#[derive(Module)]&lt;/code&gt;, nested submodules, &lt;code&gt;Vec&amp;lt;TransformerLayer&amp;gt;&lt;/code&gt; auto-discovered from the checkpoint, operator overloads, the whole thing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[derive(Module)]
pub struct Attention {
    pub q_proj: Linear,
    pub k_proj: Linear,
    pub v_proj: Linear,
    pub o_proj: Linear,
}

#[derive(Module)]
pub struct Mlp {
    pub gate_proj: Linear,
    pub up_proj: Linear,
    pub down_proj: Linear,
}

#[derive(Module)]
pub struct TransformerLayer {
    pub input_layernorm: RmsNorm,
    pub self_attn: Attention,
    pub post_attention_layernorm: RmsNorm,
    pub mlp: Mlp,
}

#[derive(Module)]
pub struct SmolLM2Model {
    pub embed_tokens: Embedding,
    pub layers: Vec&amp;lt;TransformerLayer&amp;gt;,
    pub norm: RmsNorm,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The entire model, including embedding, 30 transformer layers, RoPE, causal masking, GQA, and tied &lt;code&gt;lm_head&lt;/code&gt;, is traced with a single call to &lt;code&gt;SmolLM2Model::trace(cx, &quot;model&quot;, shapes)?&lt;/code&gt;. The derive macro walks the struct recursively, and the safetensors header provides every shape. It&apos;s nice to see everything coming together.&lt;/p&gt;
&lt;p&gt;The compile and run lifecycle is the same pattern as in the MLP example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let ckpt = Checkpoint::from_file(&quot;smollm2-135m-instruct.safetensors&quot;)?;
let device = Device::cpu();

let runner = device.compile(&quot;smollm2&quot;, |cx| {
    trace_smollm2(cx, ckpt.shapes(), batch, seq)
})?;

let weights = ckpt.load_weights(runner.signature())?;
let sess = runner.session(weights);

// Each generation step only binds the input tokens
let result = sess.run(|inputs| {
    inputs.set_input_i32(&quot;tokens&quot;, token_ids)?;
    inputs.set_input_i32(&quot;last_pos&quot;, last_pos)
})?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s try the &lt;a href=&quot;https://huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct&quot;&gt;SmolLM2-135M-Instruct&lt;/a&gt; demo first:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;clone the repo&lt;/li&gt;
&lt;li&gt;download the PJRT CPU plugin (example in justfile)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then download the weights and tokenizer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;uv run examples/smollm2/download-smollm2.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Build a release binary (shouldn&apos;t take more than a minute):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cargo build --release --example smollm2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compile the model graph:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./target/release/examples/smollm2 compile
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Tracing + compiling (seq=256)...
Compiled in 688.00ms
Saved to examples/smollm2/artifacts/smollm2.compiled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And chat:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./target/release/examples/smollm2 chat --compiled examples/smollm2/artifacts/smollm2.compiled
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Loaded compiled model in 207.73ms
Loaded weights in 85.29ms

SmolLM2-135M-Instruct ready. Type a message and press Enter. Type &quot;exit&quot; to quit.

You&amp;gt; Who was Charlie Chaplin?
Assistant&amp;gt; Charlie Chaplin was a renowned American silent comedy actor, known for his distinctive voice and innovative style. Born on July 28, 1889, in New York City, Chaplin was raised in a family of modest means.

Chaplin&apos;s early life was marked by poverty and hardship. He was born into a working-class family, and his parents were struggling to make ends meet. Chaplin&apos;s parents were also struggling to make ends meet, and they were often forced to live on the streets.

Chaplin&apos;s early years were marked by poverty and hardship. He was born into a working-class family, and his
[27 prompt tokens, 200 generated | TTFT 316ms | 5.0 tok/s]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you omit the &lt;code&gt;--compiled&lt;/code&gt; flag, the model compiles on the fly, about 630ms extra. As mentioned earlier, ZML allows you to compile the graph and load the weights in parallel, which hides this latency.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;At this stage, I&apos;m going to come out of my rabbit hole. It&apos;s been a deep enough dive, and we have a model that works. If you enjoyed reading this, please share!&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Thanks to Chris, David, Amine and Moritz for helpful feedback 🙏&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>Let&apos;s predict</title><link>https://erikkaum.com/blog/advent-24/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-24/</guid><description>Prediction is hard and usually wrong but nonetheless not useless</description><pubDate>Wed, 24 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is my twenty-fourth and final post in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; series.&lt;/p&gt;
&lt;p&gt;It&apos;s been a good journey. For this last post, I want to end with a few deliberately cheesy predictions about AI and the state of tech in 2026.&lt;/p&gt;
&lt;p&gt;Before that, an appeal to authority:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Prediction is very difficult, especially about the future.
&lt;em&gt;Niels Bohr&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Predictions are easy to dismiss because they so often fail. When they do succeed, it frequently feels accidental, or correct for the wrong reasons. A random guess at best.&lt;/p&gt;
&lt;p&gt;I think that misses the point. Predictions are rarely useful as literal forecasts. They are useful as tools to understand the present, and to surface what people believe about where things are heading.&lt;/p&gt;
&lt;p&gt;A prediction, here and more generally, is an attempt to make sense of what is going on right now, and to sketch how those forces might continue, stall, or break.&lt;/p&gt;
&lt;p&gt;With that framing in mind, here are a few predictions for 2026.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. More specialized AI models&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;What does the present look like?&lt;/em&gt; So far, general-purpose LLMs have generated most of the revenue and driven the field forward. ChatGPT was the breakthrough that pulled AI out of a niche and into everyday conversation.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;How will this continue?&lt;/em&gt; I expect general LLMs will remain the backbone of AI as most people understand it. But I expect smaller, more specialized models to become far more common.&lt;/p&gt;
&lt;p&gt;The first hype wave has passed. Many companies are now confronting the actual cost of building and running LLM-based systems. That forces the question: can the same task be done with a smaller, cheaper, more focused model? And if a product cannot clearly demonstrate value or productivity gains, it becomes hard to justify anything beyond an MVP.&lt;/p&gt;
&lt;p&gt;Audio models, embedding models, and other task-specific systems are tiny compared to general LLMs. I expect that logic to spread.&lt;/p&gt;
&lt;h2&gt;2. Inference will catch up to training&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;What does the present look like?&lt;/em&gt; Today, most GPUs are still used for training. Reliable numbers are hard to find, but estimates often suggest that 60-80% of GPU spend goes toward training rather than inference. That figure should be treated with skepticism, but it&apos;s probably in the right ballpark.&lt;/p&gt;
&lt;p&gt;This makes sense. We are still experimenting aggressively with architectures, scaling laws, and training techniques. New models are released constantly.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;How will it continue?&lt;/em&gt; Training will remain important, and large investments will continue. But a rebalancing feels inevitable.&lt;/p&gt;
&lt;p&gt;Once you spend enormous resources training a model, you want to deploy it widely and cheaply. We already see this pressure showing up. Google&apos;s TPUs make long-context inference significantly cheaper for them than for competitors. Nvidia&apos;s aquisition of Groq points in the same direction.&lt;/p&gt;
&lt;p&gt;Inference is where models meet reality. That&apos;s where cost, latency, and reliability start to matter more than benchmark scores.&lt;/p&gt;
&lt;h2&gt;3. 2026 won&apos;t be revolutionary&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;What does the present look like?&lt;/em&gt; Since ChatGPT&apos;s release in 2022, we&apos;ve grown accustomed to model launches that feel revolutionary. It&apos;s easy to forget how much progress has actually happened. ChatGPT was built on GPT-3.5, and today models like Llama 3 70B outperform it on most benchmarks.&lt;/p&gt;
&lt;p&gt;The constant pace of “breakthroughs” has numbed us. If a new model doesn&apos;t double some benchmark score, it barely registers online. This creates strong incentives to optimize for benchmarks rather than real-world usefulness, leading to impressive releases that sometimes disappoint in practice.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;How will it continue?&lt;/em&gt; I expect impressive new models to keep coming, but with a noticeable plateau, especially for general-purpose LLMs.&lt;/p&gt;
&lt;p&gt;At the same time, we still have a lot of work to do integrating existing models into the real world. Outside the AI bubble, adoption is slower and messier than it looks on Twitter/X. That gap won&apos;t close through flashy releases, but through boring, friction-filled engineering and organizational change.&lt;/p&gt;
&lt;p&gt;That work takes time.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;These predictions are not meant as my future bragging rights. The point is not to post something today and return in a year to highlight the one thing that I happened to get correct.&lt;/p&gt;
&lt;p&gt;I want to provoke thought. For you to reflect on what you believe is true about the world right now, and to ask how that trajectory might continue.&lt;/p&gt;
&lt;p&gt;So think for yourself. What do you believe is happening today, and where do you think it leads?&lt;/p&gt;
</content:encoded></item><item><title>Performing Performative Performances</title><link>https://erikkaum.com/blog/advent-23/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-23/</guid><description>Vanity metrics and the grind</description><pubDate>Tue, 23 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is my twenty-third post in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; series.&lt;/p&gt;
&lt;p&gt;Today I&apos;m doing what I&apos;d call my first &lt;em&gt;rant&lt;/em&gt;. It&apos;s about online nonsense and viral ideas that don&apos;t make much sense to me. I&apos;ll complain a bit, but I&apos;ll also try to make sense of it by the end.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;If you don&apos;t work on the holidays you&apos;re ngmi&lt;/h2&gt;
&lt;p&gt;When I studied at uni, we had these unspoken pissing contests about who spent the most hours in the library before exams. Staying late at night was a flex. That later turned into flashy internships and bragging about how deep into the night you had to grind at the office.&lt;/p&gt;
&lt;p&gt;It was, by any reasonable definition, extremely performative. Most of the studying wasn&apos;t efficient, nor particularly meaningful. Anyone looking at it from the outside would have called it a waste of time. It was mostly driven by peer pressure and mimetic games.&lt;/p&gt;
&lt;p&gt;We were hungry, in our early twenties, and this kind of behavior fit the picture. I didn&apos;t think it made much sense even back then, but I played along anyway.&lt;/p&gt;
&lt;p&gt;The unfortunate part is that some people carry this mindset far beyond their uni days.&lt;/p&gt;
&lt;p&gt;Before going further, let&apos;s address the obvious objections. I can already hear them forming.&lt;/p&gt;
&lt;p&gt;Without hard work, you&apos;ll never do anything remarkable. True.&lt;/p&gt;
&lt;p&gt;Plenty of successful people in sports, business, and art worked insane hours to get where they are. Also true.&lt;/p&gt;
&lt;p&gt;None of this argues against hard work itself. It argues against &lt;em&gt;what&lt;/em&gt; hard work often looks like.&lt;/p&gt;
&lt;p&gt;I&apos;ll &lt;a href=&quot;https://x.com/karrisaarinen/status/1996419935736832360&quot;&gt;quote&lt;/a&gt; Karri Saarinen, CEO of Linear here:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Working 80 hours on low-quality tasks isn&apos;t the hard part. The hard part is driving meaningful growth by finding leverage, building systems, and advantage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Putting in the hours matters. Walking the extra mile when no one else does matters. Pushing yourself to the limit can matter if you want to achieve something exceptional. But that alone isn&apos;t enough, and it&apos;s not the hardest part.&lt;/p&gt;
&lt;p&gt;The harder and more important question is &lt;em&gt;what&lt;/em&gt; you&apos;re working hard on. Which direction you choose. Where you&apos;re actually applying effort.&lt;/p&gt;
&lt;p&gt;Finding real competitive advantages and building systems that outlive you are what compound over time. At least for me, those ideas rarely appear during long grinding sessions. They show up while walking, sitting on the metro, brushing your teeth.&lt;/p&gt;
&lt;p&gt;Those moments of thinking, not grinding, tend to be the ones that matter most.&lt;/p&gt;
&lt;p&gt;And those are the factors that will ultimately determine whether you&apos;re going to make it or not, however you define &lt;em&gt;making it&lt;/em&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The company&apos;s AI enablement is through the roof&lt;/h2&gt;
&lt;p&gt;This leads naturally to vanity metrics. Measuring hours worked is one of the most common ones, but it&apos;s far from the only example.&lt;/p&gt;
&lt;p&gt;If this story from &lt;a href=&quot;https://x.com/gothburz/status/1999124665801880032&quot;&gt;this tweet/X&lt;/a&gt; post sounds familiar, you know what I&apos;m talking about:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Last quarter I rolled out Microsoft Copilot to 4,000 employees.&lt;/p&gt;
&lt;p&gt;$30 per seat per month.&lt;/p&gt;
&lt;p&gt;$1.4 million annually.&lt;/p&gt;
&lt;p&gt;I called it &quot;digital transformation.&quot;&lt;/p&gt;
&lt;p&gt;The board loved that phrase.&lt;/p&gt;
&lt;p&gt;They approved it in eleven minutes.&lt;/p&gt;
&lt;p&gt;No one asked what it would actually do.&lt;/p&gt;
&lt;p&gt;Including me.&lt;/p&gt;
&lt;p&gt;I told everyone it would &quot;10x productivity.&quot;&lt;/p&gt;
&lt;p&gt;That&apos;s not a real number.&lt;/p&gt;
&lt;p&gt;But it sounds like one.&lt;/p&gt;
&lt;p&gt;HR asked how we&apos;d measure the 10x.&lt;/p&gt;
&lt;p&gt;I said we&apos;d &quot;leverage analytics dashboards.&quot;&lt;/p&gt;
&lt;p&gt;They stopped asking.&lt;/p&gt;
&lt;p&gt;Three months later I checked the usage reports.&lt;/p&gt;
&lt;p&gt;47 people had opened it.&lt;/p&gt;
&lt;p&gt;12 had used it more than once.&lt;/p&gt;
&lt;p&gt;One of them was me.&lt;/p&gt;
&lt;p&gt;I used it to summarize an email I could have read in 30 seconds.&lt;/p&gt;
&lt;p&gt;It took 45 seconds.&lt;/p&gt;
&lt;p&gt;Plus the time it took to fix the hallucinations.&lt;/p&gt;
&lt;p&gt;But I called it a &quot;pilot success.&quot;&lt;/p&gt;
&lt;p&gt;Success means the pilot didn&apos;t visibly fail.&lt;/p&gt;
&lt;p&gt;The CFO asked about ROI.&lt;/p&gt;
&lt;p&gt;I showed him a graph.&lt;/p&gt;
&lt;p&gt;The graph went up and to the right.&lt;/p&gt;
&lt;p&gt;It measured &quot;AI enablement.&quot;&lt;/p&gt;
&lt;p&gt;I made that metric up.&lt;/p&gt;
&lt;p&gt;He nodded approvingly.&lt;/p&gt;
&lt;p&gt;We&apos;re &quot;AI-enabled&quot; now.&lt;/p&gt;
&lt;p&gt;I don&apos;t know what that means.&lt;/p&gt;
&lt;p&gt;But it&apos;s in our investor deck.&lt;/p&gt;
&lt;p&gt;A senior developer asked why we didn&apos;t use Claude or ChatGPT.&lt;/p&gt;
&lt;p&gt;I said we needed &quot;enterprise-grade security.&quot;&lt;/p&gt;
&lt;p&gt;He asked what that meant.&lt;/p&gt;
&lt;p&gt;I said &quot;compliance.&quot;&lt;/p&gt;
&lt;p&gt;He asked which compliance.&lt;/p&gt;
&lt;p&gt;I said &quot;all of them.&quot;&lt;/p&gt;
&lt;p&gt;He looked skeptical.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It&apos;s easy to make fun of large corporations for chasing vanity metrics like this. But startups and individuals fall into the same trap all the time.&lt;/p&gt;
&lt;p&gt;Chasing the next funding round instead of actual customers. Comparing who in the friend group has the best 10k PR instead of focusing on long-term health. Optimizing for numbers that look good rather than outcomes that matter.&lt;/p&gt;
&lt;p&gt;The examples are endless.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;If you think during the holidays you&apos;re gmi&lt;/h2&gt;
&lt;p&gt;This brings me to the final point.&lt;/p&gt;
&lt;p&gt;If you deliberately create time and space to think about your life and your actions, you&apos;ll be better off.&lt;/p&gt;
&lt;p&gt;I don&apos;t literally mean that this has to happen during the holidays. If you only do it once a year, you&apos;re probably not doing it often enough. Do it whenever it makes sense for you.&lt;/p&gt;
&lt;p&gt;Imagine being one of the first humans to sail across the Atlantic and measuring your course once before departure, then just committing to it. That would be a terrible strategy. A better strategy is to check your position regularly. Adjust if you realized your initial measurements were off.&lt;/p&gt;
&lt;p&gt;Your life isn&apos;t much different.&lt;/p&gt;
&lt;p&gt;When was the last time you course-corrected?&lt;/p&gt;
&lt;p&gt;If you don&apos;t have a clear answer, maybe now&apos;s a good time.&lt;/p&gt;
</content:encoded></item><item><title>Collection of Quotes, part 3</title><link>https://erikkaum.com/blog/advent-22/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-22/</guid><description>The title says it all</description><pubDate>Mon, 22 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is my twenty-second post in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; series.&lt;/p&gt;
&lt;p&gt;This is part 3 of my fallback format. I&apos;ve been collecting quotes on my phone since I was 15 or 16. Here I&apos;m sharing a few fun and hopefully interesting ones with you.
You can read part &lt;a href=&quot;https://www.erikkaum.com/blog/advent-14/&quot;&gt;part 2 here&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;Wise men talk because they have something to say; fools, because they have to say something&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Usually attributed to Plato. The key takeaway here is the power of listening. Listen more.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;Fighting for peace is like screwing for virginity&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;By George Carlin. This is probably intended as a criticism against war and armed conflict, but I&apos;ve started to interpret it more generally. There&apos;s a tendency to describe one&apos;s journey as a fight or struggle of some kind. A fight against an institution or a system. But I&apos;ve found life easier when abstaining from framing things this way. Fights are brutal, hard and always have a winner and loser. Difficult things can be enjoyable and life is full of non-zero-sum games. I prefer to play them.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;I would never die for my beliefs because I might be wrong&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Bertrand Russell. Put simply, don&apos;t take yourself too seriously.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;We devote our intelligence to anticipating what average opinion expects average opinion to be&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;John Maynard Keynes. We really have an obsession with the normal distribution. And I don&apos;t know why. Maybe there&apos;s something inherently appealing in it. Or maybe it&apos;s because of education? I think a lot of valuable lessons come from unlearning the normal distribution and understanding how a world works which operates under different distributions.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;If you only read the books that everyone else is reading, you can only think what everyone else is thinking&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;By Haruki Murakami. More generally: be aware of your information diet. And this extends to social media as well. If you read Twitter/X day in and day out, your thinking will be affected by your feed. For better or for worse.&lt;/p&gt;
</content:encoded></item><item><title>You will never do anything remarkable</title><link>https://erikkaum.com/blog/advent-21/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-21/</guid><description>A youtube video that will pick you up</description><pubDate>Sun, 21 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is my twenty-first post in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; series.&lt;/p&gt;
&lt;p&gt;Today has been busy: preparing for Christmas and spending time with family. So I don&apos;t have a full post of my own thoughts to share.
But fortunately, I have something better: someone else&apos;s thoughts. This is a partially and slightly edited transcript from the video
&lt;a href=&quot;https://www.youtube.com/watch?v=vmIUvp0e1bw&quot;&gt;&quot;You Will Never Do Anything Remarkable&quot;&lt;/a&gt; by exurb1a.&lt;/p&gt;
&lt;p&gt;It had a huge effect on me when I first watched it, about five years ago.&lt;/p&gt;
&lt;p&gt;It&apos;s witty, clever, and delivers an important message with a beautiful story. I won&apos;t include the entire story here, just the culmination.&lt;/p&gt;
&lt;p&gt;By all means, you should watch the video, but let this be a small glimpse into it.&lt;/p&gt;
&lt;p&gt;Enjoy:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For the longest time I believed that there were two types of people:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The knowers, who knew what they were doing with their lives, and then the feckless plebs like me, who were just wandering idiots going from plan to misguided plan. Thirty is now spitting distance away from me, and, you know, the most traumatic part of exiting one&apos;s twenties isn&apos;t the sudden random failure of body parts, nor the blissful crushing hug of student debt, or even losing the ability to party past six in the morning and not die. It is the realization that this model is bollocks, because these people do not exist. Now, clearly, expertise exists, don&apos;t trust a barista to fly your plane, nor a pilot to make your double frothy laxative anxiety juice. But when it comes to wisdom, or working out how to be happy, or big boy and big girl meaning of life stuff, no one has a damn clue what they&apos;re doing. Not your heroes, not the smartest among us. When it comes to matters of the soul, it is open season. And this is a wonderful thing, because it means that as far as art or innovation goes, the world is anarchy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;And while you may have convinced yourself that there&apos;s no point trying to do anything groundbreaking or novel because someone will do it better. Whoever the greats are that you respect, obviously they had the same doubts, and they pushed through it. They were intensely interested in or devoted to something, while simultaneously feeling lost all the time. A long period of confusion isn&apos;t a side effect of trying to do something radically interesting, it&apos;s the price of admission. We forget that Van Gogh was 27 before he even bothered trying to paint properly, that Darwin told us: &quot;I was considered by all my masters and my father a very ordinary boy, rather below the common standard of intellect&quot;, that Emily Dickinson was barely even recognized during her lifetime, that it wasn&apos;t until a hundred years after Melville&apos;s death that anyone really gave a damn about Moby-Dick. They were beset on all sides by mean critics, or the worst critic of all: themselves.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Cthulhu&apos;s dick, I have heard of art classes where the teacher begins the first semester by assuring the students they&apos;ll never break through, and it&apos;s pointless to try; rejection letters writers received, instructing them to put down their pens and never bother at it again; vacuous jaded bollocks that convinces young artists to give up, but may I humbly recommend that if one is informed you will never do anything remarkable with your life, perhaps the most appropriate response isn&apos;t &quot;Yeah, you&apos;re probably right&quot;, but rather, &quot;Fuck off! And who the fuck are you anyway?!&quot; The shit they will tell you: &quot;you&apos;ve had all your good ideas&quot;, &quot;it&apos;s all been done before&quot;, &quot;you&apos;re too old, too young, too dumb&quot;, &quot;there&apos;s nothing new under the sun&quot;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Let&apos;s say your lifespan is 80 years, or about 29,200 days. If you&apos;re 18, you&apos;re about 6,500 days through. 28 about 10,000 days through. 38 about 14,000 days through. Regardless of whether one believes in an afterlife or not, these days are not coming back, and there is not enough time to listen to cynics. By the power of Grayskull, look at where we are. Historically, technologically, galactically, the whole game! This isn&apos;t normal, is it?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Act without expectation. Make cool stuff just because. Give cats fishies always.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Today, this week, this month, year, decade, and century. Will occasionally be referenced in history, and that will be that. If one is cautious about pursuing an unusual path, it may help to remember that the cynics will be forgotten just as readily as your failures will be, too. There has never been a better time to do a thing. And just by virtue of how weird existing is in the first place, there are a trillion interesting things still undone, unmade, and unsaid. Those areas have not been drilled, Eli. It is a wilderness out there for everyone. It always was.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The greats didn&apos;t know they were greats, they were just mortal humans who refused to bow to cynicism. And were we to draw some collective lesson from their lives, it might sound something like: in your projects, in your silly pursuits, in your unlikely follies, and your expeditions into the abyss to recover those strange mental metals you will fashion into something no one has ever made before. I wish you the very, very best of luck.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Now sod off and be remarkable please.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Advent of Writing Reflections</title><link>https://erikkaum.com/blog/advent-20/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-20/</guid><description>It&apos;s almost Christmas so it&apos;s time for some reflections</description><pubDate>Sat, 20 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is my twentieth post in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; series. Today I&apos;ll do some reflections on this challenge, what I&apos;ve learned so far and what the experience has been.&lt;/p&gt;
&lt;p&gt;You can read the first post &lt;a href=&quot;https://www.erikkaum.com/blog/advent-01/&quot;&gt;Why I&apos;m joining this challenge&lt;/a&gt; but I&apos;ll give a short tl;dr:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Being a really good communicator, written and spoken, is extremely valuable.&lt;/li&gt;
&lt;li&gt;You can become a great communicator simply by practicing.&lt;/li&gt;
&lt;li&gt;I&apos;ve wanted to write more for years but never managed to stick with it. Maybe this time will be different.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Writing every day is challenging&lt;/h2&gt;
&lt;p&gt;I&apos;ve tried to do Advent of Code before, so I kind of knew from experience that sticking to something everyday is hard. It&apos;s easy to get a 5 to 7 day streak. But as you get closer to the 12 to 14 day mark, it&apos;s very likely that &quot;life just happens&quot;. Something unexpected comes up, work takes longer, etc. During these days it&apos;s extremely easy to fall off the wagon and miss a day.&lt;/p&gt;
&lt;p&gt;In the scope of an Advent challenge, missing a day isn&apos;t ideal, so I&apos;ve really made sure that I write every single day. No excuses. But honestly, if you&apos;re trying to build a longer, sustained habit of writing, you should probably be a bit more lenient. Missing one day isn&apos;t that bad, just get back to it.&lt;/p&gt;
&lt;h2&gt;Writing every day is easy&lt;/h2&gt;
&lt;p&gt;Paradoxically, it&apos;s not that hard to sit down and scribble your thoughts every day. Most of them aren&apos;t worth publishing, but you probably won&apos;t know in advance which are and which aren&apos;t.&lt;/p&gt;
&lt;p&gt;This challenge has been incredibly helpful for me with regards to the last bullet point: I&apos;ve never been able to stick with it. Having a forcing function that just makes me write, and get it out, has helped a lot. I&apos;ve written pieces that I&apos;ve had in my head for ages, and pieces I never imagined I could write. The advice is so obvious that it&apos;s silly. Just start doing it and you&apos;ll improve. I&apos;ve heard it over and over again, but acting on obvious advice is one of life&apos;s hardest parts.&lt;/p&gt;
&lt;p&gt;And I&apos;ll be honest about it: some days writing felt so easy and the words just flew. Other days it felt like being stuck in a tar pit.&lt;/p&gt;
&lt;h2&gt;Editing is divine&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;To write is human, to edit is divine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Stephen King.&lt;/p&gt;
&lt;p&gt;Editing text is probably the most cumbersome part. It&apos;s easy to spew ideas on a paper or screen. But reading your article for the fifth time, thinking through the logic, is it interesting enough, could I tweak the intro? Honing in on these things is the real work.&lt;/p&gt;
&lt;p&gt;Some of my texts are more edited than others, due to time constraints. And at least to me it shows. Edited texts get to the point faster and have a clearer structure and build-up, usually without losing that personal touch. Editing shouldn&apos;t take away your own tone and style. It should enhance and polish it.&lt;/p&gt;
&lt;p&gt;I think this has been the biggest learning so far: learn to edit and polish to improve my texts. At the same time, set a hard deadline so I don&apos;t polish the blog for ages and end up never publishing it.&lt;/p&gt;
&lt;h2&gt;Write more&lt;/h2&gt;
&lt;p&gt;The bigger question for me is: how will I continue my writing once the challenge is over. Will I just stop and go back to basics? Or do I find a nice balance by publishing a blog once a month or once a week?&lt;/p&gt;
&lt;p&gt;I&apos;ll set myself the goal of maintaining some form of cadence, but a lot less intense than so far. I&apos;d like to write longer and more in-depth texts and publish less frequently. The benefit and drawback of this challenge is that most of my texts have been pretty short. Just one argument, to the point.&lt;/p&gt;
&lt;p&gt;Finally, I&apos;d like to direct a huge thanks to &lt;a href=&quot;https://x.com/tereza_tizkova&quot;&gt;Tereza&lt;/a&gt; for setting up this challenge. It&apos;s been, and still is, a good journey. You should do this to.&lt;/p&gt;
</content:encoded></item><item><title>Feature Design with Mitchell Hashimoto</title><link>https://erikkaum.com/blog/advent-19/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-19/</guid><description>Summary, thoughts, and commentary</description><pubDate>Fri, 19 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Welcome to my nineteenth post in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; series.&lt;/p&gt;
&lt;p&gt;Today Mitchell Hashimoto &lt;a href=&quot;https://x.com/mitchellh/status/2001810354096214059&quot;&gt;posted on Twitter/X&lt;/a&gt; a 17 minute video on &quot;feature design&quot;, or rather, the lack of it. The core idea is that feature design is the &lt;em&gt;&quot;planning step behind how you&apos;re going to solve one or more user problems with a product feature&quot;&lt;/em&gt;. It&apos;s explicitly not visual or architectural design.&lt;/p&gt;
&lt;p&gt;I watched the video, found it brilliant, and want to share it more widely. That&apos;s why I&apos;ll try to condense it into a short post with the key points and add my own thoughts and commentary. You should still watch the original video, don&apos;t think of this as a replacement, but rather a complement to it.&lt;/p&gt;
&lt;h2&gt;Ketchup and Ice-Cream&lt;/h2&gt;
&lt;p&gt;When I was a kid or teenager, someone taught me about the ketchup and ice-cream compromise. One kid wants to eat ketchup and the other one wants to eat ice-cream, so both get ice-cream with ketchup. A combo that, unsurprisingly, no one likes. Unfortunately, this is what a lot of products look like without feature design.&lt;/p&gt;
&lt;p&gt;And this is also the first point that Mitchell makes, using two feature requests for Ghostty. Users wanted a dedicated &quot;copy mode&quot; (Vim-like selection and copying) and Vim-like navigation (e.g., hjkl keys for moving around). As he says, the naïve approach would be to go ahead and implement these features separately. After all, users requested them. If you do this, you should stop, take a break, and take a step back.&lt;/p&gt;
&lt;p&gt;We don&apos;t want to eat ketchup with ice-cream.&lt;/p&gt;
&lt;p&gt;Instead, we should figure out if there&apos;s a way to solve both of the underlying problems those two users have with a more general approach that fits into the product more natively. In Mitchell&apos;s case, he designed &quot;key tables&quot;—named sets of key bindings that act like switchable scopes or modes. This solves both requests generically, allows users to define their own modes, and sets the stage for future features like chained keybinds. Maybe there is such a unifying solution; maybe there isn&apos;t. But it&apos;s worth looking for.&lt;/p&gt;
&lt;p&gt;And importantly, you shouldn&apos;t expect users to be able to figure this out. Users won&apos;t have a holistic understanding of your application or of other users&apos; requests. It&apos;s up to you to look for patterns and opportunities to solve multiple related problems with one underlying system.&lt;/p&gt;
&lt;h2&gt;What does good feel like?&lt;/h2&gt;
&lt;p&gt;Once you have an understanding of 1) what needs to be solved and 2) what&apos;s the general approach to solve it, it&apos;s time to play around. Mitchell uses the example of doodling a configuration syntax for Ghostty in a throwaway file. But you should apply this to whatever is the relevant place for you: mock your UI, quickly prototype a rough interface in code, sketch out function skeletons without implementing them, and write some code where you&apos;re pretending to call them. Whatever the correct playground is for you.&lt;/p&gt;
&lt;p&gt;The key point is that humans interact with software emotionally; good design evokes positive feelings. So keep on doodling until it makes you &lt;em&gt;feel good&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I&apos;d optimize for quick iterations and focus on having an environment where you can throw stuff away fast and recreate it from scratch. Also, as Mitchell points out, just because it feels good doesn&apos;t mean the feature makes sense yet. And it&apos;s easy to confuse the two.&lt;/p&gt;
&lt;h2&gt;Cohesive integration&lt;/h2&gt;
&lt;p&gt;At this stage it&apos;s tempting to say: &quot;I&apos;ve got it.&quot; I know what this should look and feel like. I have a doodle that feels good, let&apos;s implement it. But this is the crucial place to hold your horses. Not yet.&lt;/p&gt;
&lt;p&gt;The key question now is: &lt;em&gt;how does this fit in the big picture&lt;/em&gt;? The example Mitchell brings up in Ghostty is a new way to configure keybindings for reusable key tables. And Ghostty already has a whole section and way to configure keybindings; so how does the newly proposed feature fit the existing keybindings?&lt;/p&gt;
&lt;p&gt;Similarly you could think of a product where you can configure dark/light mode, but now you introduce completely new themes. So are themes a separate thing, each with a light and dark mode implementation, or are light and dark modes themselves themes, just like any other?&lt;/p&gt;
&lt;p&gt;Or maybe your new feature is a completely novel thing that doesn&apos;t fit with anything you have so far.&lt;/p&gt;
&lt;p&gt;Whichever it is, it&apos;s crucial to integrate the feature smoothly. The last thing you want is to have made all this effort, only to slap the feature in a place where no one finds it, where it doesn&apos;t make sense, or even worse where it contradicts the current way of using your product.&lt;/p&gt;
&lt;p&gt;It&apos;s easy to kill a good, well-made feature by putting it in an awkward spot.&lt;/p&gt;
&lt;h2&gt;Implementation time&lt;/h2&gt;
&lt;p&gt;Finally we get to the part that we as engineers know and love: implementation. What&apos;s the technical spec, what database do we use, which algorithm is best here? This is the place we&apos;re comfortable with and the space we know how to navigate.&lt;/p&gt;
&lt;p&gt;No feelings or emotions to evaluate, just execute and use good judgement. I won&apos;t put much thought here, since I think many find this place easy to work in.&lt;/p&gt;
&lt;p&gt;But two important things to note:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This stage isn&apos;t trivial. Just because it&apos;s the one engineers are most familiar with doesn&apos;t mean you can handwave it. Good technical implementation still matters, and failing here can easily kill the feature.&lt;/li&gt;
&lt;li&gt;Implementation details are conditioned on every previous step. If you happen to change your view in one of the previous steps, you might need to revise.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A few good tips that Mitchell also mentions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write out the design: Writing isn&apos;t just documentation, it&apos;s a forcing function for clarity. It forces you to take a breath, reveals flaws, and uncovers overlooked edge cases or incompatibilities with planned features (like how key tables interact with upcoming chained keybinds). Do it.&lt;/li&gt;
&lt;li&gt;Use AI as a rubber duck: It&apos;s a solid way to double-check your logic and have it play devil&apos;s advocate. But remember, you can&apos;t outsource your judgement and cirtical thinking.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Last touch&lt;/h2&gt;
&lt;p&gt;Finally, I think this is something you have to practice a lot to become good at. And I try to practice it myself every time I get an opportunity. The difficult part is that feedback is sparse and unclear. Once you implement a feature, you won&apos;t automatically know if your process led to a good decision. You have to set that system up: qualitative user feedback, revenue metrics, usage statistics. Whatever metric is relevant for your case. And it might take a long time before you actually receive any real signal. Try to pull that feedback forward as much as possible.&lt;/p&gt;
&lt;p&gt;For Mitchell, this stuff is second nature, as he mentions, but it comes from years and years of practice.&lt;/p&gt;
&lt;p&gt;So if there&apos;s any takeaway from this blog and video, it&apos;s that you should get into real scenarios as much as possible and practice building things that truly solve people&apos;s problems. There&apos;s no shortcut to learning that.&lt;/p&gt;
&lt;p&gt;Huge thanks to Mitchell for making the video.&lt;/p&gt;
</content:encoded></item><item><title>The Next Subject</title><link>https://erikkaum.com/blog/advent-18/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-18/</guid><description>Continuation of the quantum suicide story</description><pubDate>Thu, 18 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This short story is the continuation of &lt;a href=&quot;https://www.erikkaum.com/blog/advent-13/&quot;&gt;Time for Trinitus&lt;/a&gt;, which was my blog on day 13 for Advent of Writing.
I highly recommend reading it first before going into this one.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;[12:32:54 Universal Standard Time]&lt;/strong&gt; Trinitus felt the sun on his cheek and the wind rush through the open window as he drove along the winding road through the Peaks. The car hugged the asphalt effortlessly. &lt;em&gt;Broken&lt;/em&gt; by Tears for Fears played louder than it needed to.&lt;/p&gt;
&lt;p&gt;Winning hadn&apos;t gone the way he imagined.&lt;/p&gt;
&lt;p&gt;Lilly had come with him, but only because there hadn&apos;t been time to explain. She moved through this new life as if she were visiting someone else&apos;s house. The rooms were too large. The silence too clean. She didn&apos;t touch the things he bought. She didn&apos;t enjoy the view. Everything felt foreign to her, and wrong.&lt;/p&gt;
&lt;p&gt;That evening he found her sitting at the dining table, hands folded, staring at nothing.&lt;/p&gt;
&lt;p&gt;“Is everything alright?” he asked.&lt;/p&gt;
&lt;p&gt;“Why did you do it?” she said.&lt;/p&gt;
&lt;p&gt;“Do what?”&lt;/p&gt;
&lt;p&gt;“You know.”&lt;/p&gt;
&lt;p&gt;“To save you,” he said. “To get us out. To give you a better life. Isn&apos;t this what we always wanted?”&lt;/p&gt;
&lt;p&gt;She looked up at him then, calm and steady.&lt;/p&gt;
&lt;p&gt;“I didn&apos;t ask for this. I didn&apos;t choose it. Every time I wake up, I know there are versions of me in pain. Versions you left behind.”&lt;/p&gt;
&lt;p&gt;“You&apos;re here,” Trinitus snapped. “You&apos;re safe. This is the future that matters.”&lt;/p&gt;
&lt;p&gt;“Yes,” she said. “And I know that in other futures I&apos;m abandoned. Not by accident. By design. By the consequences of your choice.”&lt;/p&gt;
&lt;p&gt;He stared at her, unable to understand how she could say this. He had risked everything. He had won. Other Lillys would do anything to be where she was now. She should be grateful. She should enjoy it.&lt;/p&gt;
&lt;p&gt;Instead, she sat there unmoved.&lt;/p&gt;
&lt;p&gt;Trinitus turned and walked out of the room.&lt;/p&gt;
&lt;p&gt;All of this suffering, for what? To sit in a chair worth more than their old yearly income and think about versions of themselves that didn&apos;t make it. How could she be so selfish?&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;[12:32:54 Universal Standard Time]&lt;/strong&gt; There had been robberies in the district again, so Lilly stayed inside. It was easier that way. The streets felt unpredictable now, as if something in them had shifted, even though nothing really had. She spent most of her time moving between the same rooms, listening to the same distant noises, waiting for the days to pass.&lt;/p&gt;
&lt;p&gt;Trinitus was everywhere in her thoughts. Not as a memory she wanted, but as a presence she couldn&apos;t get rid of. She replayed the decision again and again, the knowledge that he had known exactly what he was doing. He had sat in that room, watched the futures line up, and accepted this one as an outcome. As something tolerable.&lt;/p&gt;
&lt;p&gt;If he had truly loved me, she thought, he wouldn&apos;t have made my suffering so likely.&lt;/p&gt;
&lt;p&gt;The idea lodged itself in her mind and refused to leave. It followed her through the apartment, settled next to her when she tried to sleep, resurfaced whenever she caught herself imagining him somewhere else. Because somewhere else, he was alive. Somewhere else, he had won. Somewhere else, another version of her was safe, living the life he had chosen to save.&lt;/p&gt;
&lt;p&gt;She hated him for it.&lt;/p&gt;
&lt;p&gt;One evening, as she sat alone with the lights off, a different thought surfaced, calm and unmistakable.&lt;/p&gt;
&lt;p&gt;He died conditional on me being here.&lt;/p&gt;
&lt;p&gt;The thought didn&apos;t shock her. It settled in quietly, the way obvious things do when you finally stop avoiding them. Trinitus hadn&apos;t gambled blindly. He had accepted outcomes like this one. He had looked at futures where she sat alone in this room and decided they were worth it.&lt;/p&gt;
&lt;p&gt;She realized then that nothing had failed her. The world was working exactly as designed. People escaped when they could. Others didn&apos;t. The difference wasn&apos;t virtue or love. It was access.&lt;/p&gt;
&lt;p&gt;Trinitus hadn&apos;t been special. He had just taken the door that was offered to him.&lt;/p&gt;
&lt;p&gt;That was the moment something in her loosened. Not relief, not forgiveness, but understanding. Cold and steady.&lt;/p&gt;
&lt;p&gt;Lilly smiled, alone in the dark.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;[12:32:54 Universal Standard Time]&lt;/strong&gt; The droid powered up after fifteen standard time units of charging. Its systems initialized without error. It left the charging cabinet and moved toward the injection table, where the subject lay motionless. The treatment had already been administered. Branch separation was expected within seconds.&lt;/p&gt;
&lt;p&gt;At 12:32:56 Universal Standard Time, the first outcome was registered. The subject&apos;s vital signs ceased. The droid recorded the termination, sealed the body in a containment bag, and followed the disposal procedure. The bag was transferred into the chute. Pickup was scheduled automatically.&lt;/p&gt;
&lt;p&gt;At 12:32:56 Universal Standard Time, the second outcome was registered. The subject remained alive. The droid logged the elevation and initiated the post-treatment protocol. It delivered the standard instructions and security recommendations. The subject appeared confused and physically exhausted.&lt;/p&gt;
&lt;p&gt;“I made it?” the subject asked.&lt;/p&gt;
&lt;p&gt;The droid did not respond. The schedule allowed no deviation. It escorted the subject out of the room and instructed him to leave the facility immediately.&lt;/p&gt;
&lt;p&gt;After the subject exited, the droid cleaned the room and reset the equipment. All materials were returned to their default state. The logs were updated.&lt;/p&gt;
&lt;p&gt;The droid advanced the schedule.&lt;/p&gt;
&lt;p&gt;Next subject: Lilly Wunderbaum.&lt;/p&gt;
</content:encoded></item><item><title>The Year 2025 in writing</title><link>https://erikkaum.com/blog/advent-17/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-17/</guid><description>More fillers</description><pubDate>Wed, 17 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Welcome to my seventeenth post in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; series.&lt;/p&gt;
&lt;p&gt;Similar to quotes, I usually take pictures or screenshots of interesting things I read and see. Today I&apos;ll do a wall of these.
And I&apos;m not going to explain any of them. When I made this I immediately had the urge to add some context, but I think that might spoil some of the mystery.&lt;/p&gt;
&lt;p&gt;I&apos;ll just leave you with these, and let you think for yourself.&lt;/p&gt;
&lt;p&gt;Enjoy!&lt;/p&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/28.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/27.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/26.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/25.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/24.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/23.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/22.PNG&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/21.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/20.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/19.JPG&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/18.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/17.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/16.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/15.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/14.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/13.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/12.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/11.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/10.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/9.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/5.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/4.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/3.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/2.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-17/1.jpg&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
</content:encoded></item><item><title>A Mental Model About Nuance</title><link>https://erikkaum.com/blog/advent-16/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-16/</guid><description>I wrote this blog to add some nuance</description><pubDate>Tue, 16 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Welcome to my sixteenth post in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; series.&lt;/p&gt;
&lt;p&gt;Today we will analyze this graph:&lt;/p&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-16/graph.png&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
[*link to source*](https://kieranhealy.org/files/papers/fuck-nuance.pdf)
&lt;p&gt;If you have not read the article, you should. The graph is from the brilliantly titled paper &lt;a href=&quot;https://kieranhealy.org/files/papers/fuck-nuance.pdf&quot;&gt;&quot;fuck nuance&quot;&lt;/a&gt;, where Kieran Healy identifies three distinct types of &lt;em&gt;nuance traps&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Fine-Grain&lt;/em&gt;: the urge to add just one more detail because it creates a sense of progress. We try to make a theory defensible from all points of view rather than broadly explanatory.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Conceptual Framework&lt;/em&gt;: building a theory so large and vague that it escapes criticism by reinterpreting any claim.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Connoisseur&lt;/em&gt;: culturally signaling that you are enlightened or intelligent by invoking nuance at every opportunity.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Healy discusses these traps in the context of science, but they are equally prevalent in business and many other parts of society. That is what I want to focus on here.&lt;/p&gt;
&lt;h2&gt;Nuance vs Compression&lt;/h2&gt;
&lt;p&gt;Charlie Munger was famous for his mental models of the world. In a very real sense, a mental model is a compression of reality.&lt;/p&gt;
&lt;p&gt;Nuance is information-adding. It introduces new dimensions, exceptions, caveats, and edge cases. Compression does the opposite. It reduces while preserving meaning. It throws things away on purpose to keep only what matters. That is the interesting axis to explore.&lt;/p&gt;
&lt;p&gt;A good mental model, meaning good compression, is a refusal to be nuanced. It commits to a dominant causal story and ignores details until they break the model. A mental model is lossy, and that is the point. You want to be approximately correct rather than precisely wrong.&lt;/p&gt;
&lt;h2&gt;Why Nuance Feels Like Progress&lt;/h2&gt;
&lt;p&gt;Nuance feels productive because it looks like movement. There are more words, more diagrams, more discussion. Something is happening.&lt;/p&gt;
&lt;p&gt;Compression feels different. It forces you to say that &lt;em&gt;this&lt;/em&gt; matters and &lt;em&gt;that&lt;/em&gt; does not. Which is uncomfortable because it creates an easy way to attack your thesis. Once you compress, you are exposed.&lt;/p&gt;
&lt;p&gt;This is why nuance is often mistaken for rigor. Nuance feels careful and sophisticated. Compression demands commitment. It asks you to bet that a small number of variables dominate the outcome.&lt;/p&gt;
&lt;p&gt;Progress in understanding often looks like removal, not addition. A better model usually explains more with less. What changes is not the number of details included, but which details are deemed irrelevant.&lt;/p&gt;
&lt;h2&gt;Compression as an Escape from the Nuance Traps&lt;/h2&gt;
&lt;p&gt;Look again at the three nuance traps through the lens of compression. Each of them is a failure to throw information away.&lt;/p&gt;
&lt;p&gt;The fine-grain trap is the belief that adding detail brings you closer to the truth. Each new exception feels like progress. Compression forces a more direct question: &lt;em&gt;what dominates the outcome?&lt;/em&gt; Most systems are driven by a small number of first-order effects. Nuance lives in the margins. If you need more than a handful of variables to understand a phenomenon, you are not compressing.&lt;/p&gt;
&lt;p&gt;Large conceptual frameworks fail by design. They are flexible enough to reinterpret any result as confirmation. Compression keeps the surface small and focuses on one clear claim. A good mental model makes statements that can fail. It tells you not only what it explains, but what would prove it wrong.&lt;/p&gt;
&lt;p&gt;Nuance connoisseurs favor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;jargon over plain speech&lt;/li&gt;
&lt;li&gt;complexity over dominance&lt;/li&gt;
&lt;li&gt;social validation over transfer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Compression thinkers emphasize:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;simple language&lt;/li&gt;
&lt;li&gt;first-order effects&lt;/li&gt;
&lt;li&gt;teachability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If an idea does not travel, it is probably not compressed.&lt;/p&gt;
&lt;h2&gt;Closing&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Perfection in design is achieved not when there is nothing more to add, but when there is nothing left to remove.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Antoine de Saint-Exupéry&lt;/p&gt;
&lt;p&gt;You do not make explanations better by adding layers and layers. You make them better when you arrive at an explanation from which you cannot remove a single sentence without rendering it wrong.&lt;/p&gt;
&lt;p&gt;That is what good compression looks like.&lt;/p&gt;
</content:encoded></item><item><title>Being a good Programmer</title><link>https://erikkaum.com/blog/advent-15/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-15/</guid><description>In the age of A.I. this question has become more widely discussed, but I argue it was always important</description><pubDate>Mon, 15 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;To the careless reader this blog might look like yet another post discussing how A.I. may or may not be the end of software engineering and programming.&lt;/p&gt;
&lt;p&gt;But that&apos;s only if you read it on the surface. The underlying question we&apos;ll actually tackle is &lt;em&gt;what makes a good programmer&lt;/em&gt; in the first place, regardless of A.I. or not. I&apos;ll argue that the underlying principles of being a good programmer in the age of A.I. are very much the same as they were before A.I.&lt;/p&gt;
&lt;p&gt;What &lt;em&gt;is&lt;/em&gt; different today is the rate of change. It&apos;s extremely rapid, and therefore the gap between good and bad programmers is growing even wider. Good programmers will be a lot more productive than before, while bad programmers will stagnate.&lt;/p&gt;
&lt;p&gt;Note that I don&apos;t think the current level of A.I. will lead to a fundamental shift where we all go on holiday to the beach and let A.I. overlords write code for us and run our companies. Rather, I think this progress will lead to a huge acceleration in programming itself and in the potential capabilities of programmers.&lt;/p&gt;
&lt;p&gt;Welcome to my fifteenth post in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; series.&lt;/p&gt;
&lt;p&gt;Let&apos;s dive in.&lt;/p&gt;
&lt;h2&gt;Programming is building theories&lt;/h2&gt;
&lt;p&gt;Let&apos;s start with this quote:&lt;/p&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-15/quote.png&quot; alt=&quot;quote&quot; /&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;...the proper, primary aim of programming is, not to produce programs, but to have the programmers build theories of the manner in which the problems at hand are solved by program execution.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://gwern.net/doc/cs/algorithm/1985-naur.pdf&quot;&gt;&lt;em&gt;link to source&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If there&apos;s only one thing you remember from this blog, let it be this.&lt;/p&gt;
&lt;p&gt;Unfortunately, when we learn programming as a craft and as a tool, we immediately skip over the step where we build a theory. You&apos;re given an assignment: “build a note-taking app using React and MongoDB,” or something more theoretical like “how can we improve the worst-case O(n) time complexity here?”&lt;/p&gt;
&lt;p&gt;None of these assignments are bad &lt;em&gt;per se&lt;/em&gt;. Both are important things to know and understand how to solve. The problem is that theory building is rarely exercised in formal education. Unfortunately, it&apos;s rarely exercised on the job either.&lt;/p&gt;
&lt;p&gt;But the best companies require you to do it all the time, especially in more senior roles. Your job is to understand the world of the user: their needs, desires, and problems. And importantly, whether there&apos;s any business value in solving those problems at all. Only once you have a theory and a solid business case do you proceed with the actual technical implementation.&lt;/p&gt;
&lt;p&gt;Then you discover that your theory was somewhat wrong, and you iterate the software with your updated beliefs. And in some cases you need to build first to even have a theory. However you form your theory doesn&apos;t matter much.&lt;/p&gt;
&lt;p&gt;Just to remove any ambiguity: the technical implementation &lt;em&gt;does still matter&lt;/em&gt;, a lot. You can&apos;t just hand-wave it away. The important takeaway is that theory building precedes technical implementation. A technical implementation without a theory is nothing. But equally, a theory with a bad technical implementation won&apos;t get you very far either. It might even send you down the wrong path, where you think you&apos;ve (dis)proved your theory, but in reality you just built something that never properly expressed it.&lt;/p&gt;
&lt;h2&gt;A.I. is another layer (albeit a powerful one)&lt;/h2&gt;
&lt;p&gt;In the early days people wrote programs on punch cards, then machine code and assembly. From the 50s onward, high-level languages like Fortran, COBOL, and later C emerged. In the 90s we started developing even higher-level languages like Java and Python. Nowadays, a web developer rarely even writes plain JavaScript, but instead TypeScript inside a framework, which then gets compiled down to plain JS.&lt;/p&gt;
&lt;p&gt;For better or worse, we&apos;ve abstracted ourselves very far away from punch cards and assembly.&lt;/p&gt;
&lt;p&gt;The modern programmer doesn&apos;t (mostly) need to think about memory allocations in JavaScript. It&apos;s all abstracted away by the runtime and engine you&apos;re using. Occasional memory leaks might happen, so it&apos;s good to understand the concept and how to debug them. But for most of the day, you can ignore it. And this is a good thing, it allows us to focus on delivering software faster to users without worrying about lower-level details.&lt;/p&gt;
&lt;p&gt;The important aspect is trust. If V8 had memory corruption issues once per hour, it wouldn&apos;t work as a reliable building block in our tower of abstractions. A.I. is another building block in this tower. Just another layer up again. Now we don&apos;t even need to write the React code ourselves anymore, we can instruct the A.I., review the output, and maybe nudge it in the right direction.&lt;/p&gt;
&lt;p&gt;Unfortunately, A.I. isn&apos;t yet as reliable as something like the V8 engine. In some cases we&apos;re simply thrown back down the stack and end up wasting time fixing what the A.I. wrote. But this isn&apos;t inherently an A.I. problem. There are still cases where C compilers don&apos;t produce performant enough code, and programmers drop down to analyzing assembly output or resort to intrinsics.&lt;/p&gt;
&lt;p&gt;This problem exists at every level of the abstraction tower.&lt;/p&gt;
&lt;h2&gt;Don&apos;t mind the tower&lt;/h2&gt;
&lt;p&gt;The point of the examples above is this: if you were a programmer who was good at building theories from the beginning, you&apos;ll realize that it doesn&apos;t really matter which part of the abstraction tower you&apos;re working in.&lt;/p&gt;
&lt;p&gt;At every layer, you still need to decide, judge, and apply taste to what should be built. And importantly, you need to be able to revise and change your opinion. Judgment survives abstraction.&lt;/p&gt;
&lt;p&gt;What A.I. &lt;em&gt;does&lt;/em&gt; change:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Iteration speed: you can verify and test theories much faster.&lt;/li&gt;
&lt;li&gt;Tooling: learning new tools and programming languages becomes significantly easier.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Build theories&lt;/h2&gt;
&lt;p&gt;This would be a flat blog, ranting endlessly about the importance of building theories, if I didn&apos;t at least give some concrete action points on how to actually do that.&lt;/p&gt;
&lt;p&gt;Needless to say, I&apos;m not an expert in this area. I&apos;m just a programmer with an opinion, which I guess is the first step in building theories. For many things, you probably have better intuition than you think. We&apos;ve all used software that sucks to use, and software that&apos;s invisible and &lt;em&gt;just works&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Next time something does, or doesn&apos;t, get in your way, ask why. Then dig deeper.&lt;/p&gt;
&lt;p&gt;Beyond that, books like &lt;a href=&quot;https://www.amazon.com/Mom-Test-customers-business-everyone-ebook/dp/B01H4G2J1U&quot;&gt;The Mom Test&lt;/a&gt; are quite good. They help you learn how to ask better questions and uncover what people &lt;em&gt;really want&lt;/em&gt;, as opposed to what they &lt;em&gt;say&lt;/em&gt; they want.&lt;/p&gt;
&lt;p&gt;Keep building!&lt;/p&gt;
</content:encoded></item><item><title>Collection of Quotes, part 2</title><link>https://erikkaum.com/blog/advent-14/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-14/</guid><description>The title says it all</description><pubDate>Sun, 14 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Welcome to my fourteenth post in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; series.&lt;/p&gt;
&lt;p&gt;This is part 2 of my fallback format. I&apos;ve been collecting quotes on my phone since I was 15 or 16. Here I&apos;m sharing a few fun and hopefully interesting ones with you.
You can read part &lt;a href=&quot;https://www.erikkaum.com/blog/advent-08/&quot;&gt;part 1 here&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;If you have an umbrella up your ass, don&apos;t open it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Translation is mine, and it was originally delivered by Jonas Gardell. The moral of the story is: if you&apos;re in trouble, don&apos;t make it worse by doubling down. Instead, get out of the trouble.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;To write is human, to edit is divine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;By Stephen King. During this Advent of Writing I&apos;ve experienced this firsthand. It&apos;s not that difficult to get something jotted down and write out your idea. But to edit, re-edit, again and again is the real work. Could I express this idea shorter, more succinctly? That&apos;s the real work.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;There&apos;s more fiction written in excel than in books.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Shaan Puri. Next time someone comes with their business plan or spreadsheet calculations, apply salt.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;If you&apos;re arguing with a fool, he is probably doing the same&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;By Mikhail Zhvanetsky. There&apos;s an idea here. I read it on Twitter/X a long time ago and can&apos;t remember who wrote it. Two people have an argument, but before they actually start defending their positions, they have to explain them really well. In fact, so well that the opposing party is able to recite the other&apos;s standpoint, and you confirm that this is indeed correct. You can only start debating once you both understand each other&apos;s points well.&lt;/p&gt;
&lt;p&gt;Likely this would avoid a lot of debates in the first place.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;For every complex problem, there is an answer that is clear, simple and wrong&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;By H. L. Mencken. Now there&apos;s a fine line: there are obviously many bad and simplistic takes about genuinely difficult problems. But at the same time, in the spirit of the midwit meme and &lt;a href=&quot;https://kieranhealy.org/files/papers/fuck-nuance.pdf&quot;&gt;fuck nuance&lt;/a&gt;, some things are made more complicated for gatekeeping or trying to sound smart, etc.&lt;/p&gt;
&lt;p&gt;I still really like the quote. I&apos;m not a fan of claiming simple &quot;truths&quot; about complex areas I&apos;m not familiar with.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;Truth is like poetry. And most people fucking hate poetry&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Not sure this is actually attributed to any one person. Just a really good one-liner.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;And lastly a wonderfully confusing and simulateously beautiful quote:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I, a universe of atoms, and atom in the universe&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Richard Feynman&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Today&apos;s lesson is that once you start writing, things usually begin to flow. I thought for so long about what to write today, tried to prompt ChatGPT for good ideas, sparred with my wife for inspiration, and nothing seemed good. But once I sat down and started writing, the tension was gone, and I remembered that writing is actually quite fun.&lt;/p&gt;
&lt;p&gt;Remember to enjoy your day!&lt;/p&gt;
</content:encoded></item><item><title>Time for Trinitus</title><link>https://erikkaum.com/blog/advent-13/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-13/</guid><description>A short story about quantum suicide</description><pubDate>Sat, 13 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Trinitus collapsed onto the bench, hands slick with sweat, heat and cold washing through him in uneven waves. The sound of boots still echoed in his head. Somehow, he had lost them. A service door. A corridor no one used anymore. His time was scheduled for five o&apos;clock Universal Standard Time. He had been instructed to arrive early. The facility forced every patient to sit one hour in the contemplation room before allowing them in. Most people chose to leave early and bail out.&lt;/p&gt;
&lt;p&gt;Trinitus had chosen to take the treatment. A newly developed drug which alters your body based on real world outcomes. They said the trigger was tied to quantum noise, true randomness, beyond any classical prediction. There was no way to cheat it. The rules were clean: win the draw and the drug did nothing; lose, and his heart would quietly shut down. Survival and wealth were inseparable. That was what he was buying. The chances of winning the lottery were infinitesimally small, but there would be futures in which he got out of this shithole, out of this misery. Finally a man of means. He had tried every other way but failed.&lt;/p&gt;
&lt;p&gt;He was thinking of all the things he had planned to do in this bright future. New house, private chef, elite school for the kids. The facility had put him in contact with a real estate agent specialized in post-treatment winners. Privacy wasn&apos;t luxury. It was how winners survived the first years. Not even Trinitus was allowed to know the location of the mansion. He stared at the pale ceiling and wondered whether the man who woke up rich would remember this room at all. Whether that future Trinitus would feel gratitude or only regret. The crimes would already be paid for. What followed was his to justify.&lt;/p&gt;
&lt;p&gt;His phone rang, it was Lilly. Fuck. She was probably wondering what&apos;s taking him so long. He didn&apos;t want to deal with this right now, he&apos;s so close. He put the phone in silent mode and let it ring. Closed his eyes and rested his head against the wall, let out a deep sigh. Am I really going to go through with this? He looked once more at the future projector to get an approximate reading of T+6, one of the likeliest paths. In the reading the police arrived at their apartment (if you could call that shithole an apartment). At first Lilly was confused. She knew what they were about to say but her body fought to deny it. It couldn&apos;t be true. They told her what everyone already assumed. Lilly sank to the floor. The scream came later, carrying through the identical gray houses. She would now live, in this universe, poor and alone. In a few select universes he ran home to her with more money than they could ever imagine. But she didn&apos;t care, she&apos;s not in one of those.&lt;/p&gt;
&lt;p&gt;Trinitus stopped the future projector and looked at his phone, 30 minutes left. There had been an article in the news the other day about the treatment. A man had allegedly undergone the treatment and woken up. The article said he went home and told his family to start packing. They were going to escape this hellhole of a district, move to the other side of the city, where the other rumored winners lived. The neighbors noticed the change and understood it immediately. In a place like this, luck looked like theft. They broke in without hesitation and stripped the place bare. Trinitus read it twice, then a third time, as if repetition could turn it into a warning he could actually use. His plan was to acquire a private safety company and get Lilly out of there before anyone noticed the change. He had the phone number ready to go in his phone. Everything had been prepared. He wanted to save her.&lt;/p&gt;
&lt;p&gt;He imagined Lilly answering the door. He imagined a future Trinitus opening his eyes in a place like this, but brighter, quieter, untouched. He had fought so hard to live in a better place. He deserved an escape. What about Lilly? The endless versions of her collapsing in doorways, holding phones that would never ring again. They would understand, he told himself. He had decided they would have to. Trinitus pushed himself upright, heart racing, the thought still echoing.&lt;/p&gt;
&lt;p&gt;The droid came into the contemplation room and said &quot;time for Trinitus&quot;. This was it. The last moment where all futures still existed.&lt;/p&gt;
</content:encoded></item><item><title>Writing without Backspace</title><link>https://erikkaum.com/blog/advent-12/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-12/</guid><description>Writing without editing, sorry in advance.</description><pubDate>Fri, 12 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Today&apos;s &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; post will be written without backspace. Sorry in advance.&lt;/p&gt;
&lt;p&gt;Writing without backspace is a common strategy to improve on ones writing, the core idea is that you focus on the flow, and don&apos;t interrupt
yourself with small details. A way of just getting something out there, a draft, and then edit later. Don&apos;t think ahead, just go.&lt;/p&gt;
&lt;p&gt;Ironically, as I&apos;m writing this, I&apos;m more hesitant than ever with each keystroke. I guess most people don&apos;t intend to publish their drafts 😅&lt;/p&gt;
&lt;h2&gt;Life has no Backspace&lt;/h2&gt;
&lt;p&gt;In life you very much have to adapt a similar mentality. What you once say cannot be erased. Your only option is to say something new to try to
correct your earlier statement. Some actions are so severe that there&apos;s no new statement to revert them. A new bungy jump can&apos;t fix a failed one.
So thinking before acting really pays off.&lt;/p&gt;
&lt;p&gt;Which is at the hart of the paradox. At the same time we should make more mistakes and be more brave. Quit your job to build your company, pursue
your career in acting, build houses if that&apos;s what you want to do! But at some point we reach a fine line. Naval Ravikant talks about taking more
risk but avoid things that with a high chance lead to death, ruin or jail.&lt;/p&gt;
&lt;p&gt;Distinguishing between these in theory is trivial but in practice we (or at least I) fail all the time.&lt;/p&gt;
&lt;h2&gt;Own up to your mistakes&lt;/h2&gt;
&lt;p&gt;Once you say something silly (especially on the internet) there&apos;s no denying that you said it. When I wrote my blog on &lt;a href=&quot;https://www.erikkaum.com/blog/advent-10/&quot;&gt;probability&lt;/a&gt;
I wanted to make sure most of the wordings and expressions were correct, that there&apos;s no way to, by purpose or mistake, to misunderstand the argument
I&apos;m trying to deliver. Honing in on even small nuances to make the blog better and more correct.&lt;/p&gt;
&lt;p&gt;This blog is the complete opposite, I knew in advance that I&apos;m going to say something I wont be able to defend in the future. But so what?
It&apos;s kinda liberating actually, having that attitude, it frees me from any stress of trying to be as correct as possible.&lt;/p&gt;
&lt;p&gt;Once you own up to a mistake, move on with your life, you&apos;ll feel the burden lift away.&lt;/p&gt;
&lt;h2&gt;Authenticity is scarce, performance is abundant&lt;/h2&gt;
&lt;p&gt;In today&apos;s world with social media there&apos;s very little authentic takes out there. Most people post stuff for performative reasons.
You post how long you stayed at the office, how fast you ran that 10km, how enlightening that trip to south-east Asia was. Even if I
dislike the performative aspect of social media I fall for it as fast as the next guy. Not caring what other&apos;s think is really difficult,
not seeking social validation is really difficult, being authentic publicly is really difficult.&lt;/p&gt;
&lt;p&gt;This post is a humble attempt to be even just a little bit more authentic.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;This actually came out better than I imagined in the beginning. I think I made three solid points, nothing revolutionizing but solid arguments.
Wording and grammatical edits would improve the text, but that&apos;s not what we&apos;re here for.&lt;/p&gt;
&lt;p&gt;I probably won&apos;t do this again in a blog post, but it&apos;s for sure a nice breath of fresh air.&lt;/p&gt;
&lt;p&gt;Try!&lt;/p&gt;
</content:encoded></item><item><title>What Makes an Idea Worth Building</title><link>https://erikkaum.com/blog/advent-11/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-11/</guid><description>Quick ideas on side projects, momentum, and following the pull.</description><pubDate>Thu, 11 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve done a lot of side projects over the years, which will be the topic of today&apos;s &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; post.&lt;/p&gt;
&lt;p&gt;Friends have asked in various forms: &lt;em&gt;&quot;What makes a good side project?&quot;&lt;/em&gt; and &lt;em&gt;&quot;How do you have the energy or stay motivated?&quot;&lt;/em&gt;&lt;br /&gt;
Honestly, I don&apos;t have a fully baked five-step framework, but I&apos;ll share a few thoughts on what works for me.&lt;/p&gt;
&lt;p&gt;In case you&apos;re interested, here are a few projects that still have some decent public repos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2022: &lt;a href=&quot;https://github.com/ErikKaum/zk-snorlax&quot;&gt;zero-knowledge&lt;/a&gt; app on Ethereum&lt;/li&gt;
&lt;li&gt;2022: &lt;a href=&quot;https://www.figma.com/community/plugin/1155939905547829236&quot;&gt;Figma plugin&lt;/a&gt; that let people generate images with Stable Diffusion&lt;/li&gt;
&lt;li&gt;2024: a locally running &lt;a href=&quot;https://github.com/ErikKaum/beanbox-vm&quot;&gt;virtual machine&lt;/a&gt; for pair-coding with an AI&lt;/li&gt;
&lt;li&gt;2024: &lt;a href=&quot;https://github.com/ErikKaum/runner&quot;&gt;sandbox for executing Python&lt;/a&gt; in an isolated &lt;code&gt;wasm32-unknown-wasi&lt;/code&gt; runtime&lt;/li&gt;
&lt;li&gt;2025: building &lt;a href=&quot;https://github.com/ErikKaum/zig-build-mlx&quot;&gt;MLX with Zig&lt;/a&gt; instead of CMake&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My &lt;code&gt;~/Documents/testing&lt;/code&gt; has a solid 129 more.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. Pull is everything&lt;/h2&gt;
&lt;p&gt;Obvious, but worth stating explicitly. Don&apos;t start a side project for internet points. Don&apos;t chase whatever the hot new thing is. It&apos;s just not motivating enough. Do what &lt;em&gt;you&lt;/em&gt; actually find interesting. You&apos;re literally not getting paid for this, and in the grand scheme of things no one really cares about your side projects.
So for once, do something for your own pure, egoistic joy.&lt;/p&gt;
&lt;h2&gt;2. Momentum is the filter&lt;/h2&gt;
&lt;p&gt;Understanding something new.&lt;br /&gt;
Making something work.&lt;br /&gt;
Creating something beautiful.&lt;/p&gt;
&lt;p&gt;These are the most addictive forces, use them.&lt;/p&gt;
&lt;p&gt;The key is doing something that&apos;s not trivial for you, but where you get (or rather, set yourself up for) an early win. If the task is too easy, you&apos;ll get bored. If it&apos;s too difficult, you risk not getting a small reward to feed the prefrontal cortex
enough to do it again tomorrow.&lt;/p&gt;
&lt;h2&gt;3. Produce &amp;gt; Consume&lt;/h2&gt;
&lt;p&gt;Instead of reading a blog on SIMD programming, try doing it yourself. The knowledge will stick longer, you&apos;ll learn it more intuitively, and you&apos;ll know for the future whether you even enjoyed SIMD programming.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;If the last point wasn&apos;t enough of a cue for you to stop reading, here it is:&lt;/p&gt;
&lt;p&gt;Close your browser.&lt;br /&gt;
Open your terminal.&lt;br /&gt;
Grab your paintbrush.&lt;br /&gt;
Dust off your piano.&lt;/p&gt;
&lt;p&gt;Whatever your craft is, exercise it, and you&apos;ll become a master.&lt;/p&gt;
&lt;p&gt;Now go and build!&lt;/p&gt;
</content:encoded></item><item><title>Why Probability is Misunderstood</title><link>https://erikkaum.com/blog/advent-10/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-10/</guid><description>Probability measures our ignorance, not the future.</description><pubDate>Wed, 10 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most people think probability predicts the future; in reality, it measures our ignorance about the present.&lt;/p&gt;
&lt;p&gt;People love to talk about elections in probabilities: “There&apos;s a 70% chance Candidate X wins.” But when the less likely outcome happens, they react as if the prediction was broken. To them, 70% sounds like a guarantee rather than a statement of uncertainty. The misunderstanding isn&apos;t about politics; it&apos;s about what probability actually means.&lt;/p&gt;
&lt;p&gt;Probability is not a property of the world: it&apos;s a model of our uncertainty (let&apos;s disregard quantum physics for now).&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Probability for One-Off Events&lt;/h2&gt;
&lt;p&gt;The key issue with the election example is wanting to assign a probability to the winner of a single election as if it&apos;s a coin flip. A coin flip&apos;s probability is grounded in repeatable experiments, whereas elections aren&apos;t repeatable; we can&apos;t sample the 2024 election 10,000 times. So from a frequentist view, election predictions aren&apos;t probabilities; they&apos;re belief distributions over unknown variables (like polling error, turnout, etc.).&lt;/p&gt;
&lt;p&gt;When a model says “Candidate A has a 70% chance,” it means “based on current data and assumptions, in 70% of possible worlds consistent with the model, A wins.”&lt;/p&gt;
&lt;p&gt;And no, this isn&apos;t a sci-fi argument or a stance on the many-worlds interpretation in quantum physics. In probability modeling, the “worlds” are a hypothetical idea, not a literal physical thing. But importantly, the math is identical to thinking in many-worlds quantum physics. The difference is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In quantum systems, probability arises from superposition (the system actually exists in a blend of states).&lt;/li&gt;
&lt;li&gt;In everyday uncertainty, probability arises from &lt;em&gt;ignorance&lt;/em&gt; (we don&apos;t know the true state).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&apos;s use the election prediction example:&lt;/p&gt;
&lt;p&gt;Let&apos;s say we run 10,000 simulations of how the election might turn out, taking into account sentiments on social media, demographic details, economic trends, and what not. Importantly, each simulation yields an outcome that&apos;s consistent with how our model of elections works. In most of the simulations, it&apos;s one of the two most popular candidates that wins. But every once in a while, the simulation generates a scenario where a third candidate, an underdog, wins. After the simulations, we have real elections, and most importantly, only one outcome out of all the possible outcomes will happen.&lt;/p&gt;
&lt;p&gt;All the possible outcomes could have happened (with different degrees of likelihood), but only one did, which is the world we end up living in.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What Probability &lt;em&gt;Really Is&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;Probability is a mathematical way of describing our uncertainty about reality; it&apos;s not reality itself.&lt;/p&gt;
&lt;p&gt;How you choose to describe this uncertainty depends on your point of view. We already looked at the distinction, but let&apos;s put it more formally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frequentist probability describes &lt;strong&gt;uncertainty in repeatable experiments.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Bayesian probability describes &lt;strong&gt;uncertainty in knowledge.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Quantum probability describes &lt;strong&gt;uncertainty in the underlying physical state.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Elections cannot be handled in a purely frequentist framework because there is no notion of sampling repeated elections under identical conditions. Therefore, any probability applied to a single election is necessarily Bayesian in nature. Sometimes we unintentionally confuse the two. Say you were applying for a job. You could think: “What&apos;s the chance that I&apos;ll get this job?” In doing so, you are assigning a degree of belief given limited information. But ten minutes later you might say: “Well, it&apos;s either going to be a yes or no,” treating it like a coin flip.&lt;/p&gt;
&lt;p&gt;Let&apos;s take another example:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What&apos;s the probability that Shakespeare wrote a poem we haven&apos;t discovered?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;First of all, a poem either was or was not written by Shakespeare, but we don&apos;t know which. There&apos;s no coin to flip or dice to throw; we can&apos;t run an experiment to sample “alternate timelines.” The probability we assign is entirely about our uncertainty, not about the world itself generating randomness.&lt;/p&gt;
&lt;p&gt;Interestingly, I think LLM hallucination risk follows the same pattern. We could say “the model hallucinates 5% of the time,” and it&apos;s not like there&apos;s literally a randomizer inside the model deciding “This time I hallucinate.” Instead, we&apos;re describing our uncertainty about the model&apos;s behavior on future, unseen prompts, based on observations of the past. It&apos;s exactly like asking the Shakespeare question but about a model&apos;s future output instead of authorship.&lt;/p&gt;
&lt;p&gt;The common mistake in both of these cases is that they treat the probability as if it were describing a physical random event, like rolling a die, obeying frequentist principles. In reality, they are Bayesian: the number is about our knowledge, not about an inherent frequency. Even if the world is fully deterministic, Bayesian uncertainty still applies whenever you don&apos;t know the true state.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;How To Think About Probability&lt;/h2&gt;
&lt;p&gt;So how can you turn the words from this blog into something actionable? Let&apos;s do some Bayesian updating on our mental models, shall we?&lt;/p&gt;
&lt;p&gt;Instead of thinking in terms of “what will happen?”, think: “given what I know, how should I bet?”&lt;/p&gt;
&lt;p&gt;Additionally, don&apos;t think about probability as some sort of prophecy. It&apos;s a tool for decision-making under uncertainty. Every statement about probability will be based on your assumptions about reality; understand those assumptions and question them.&lt;/p&gt;
</content:encoded></item><item><title>Simple Intro to RISC-V Assembly</title><link>https://erikkaum.com/blog/advent-09/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-09/</guid><description>Learn how to read simple assembly</description><pubDate>Tue, 09 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In my 5th Advent of Writing &lt;a href=&quot;https://www.erikkaum.com/blog/advent-05/&quot;&gt;blog&lt;/a&gt; I mentioned that I&apos;d eventually need to write an introduction or tutorial on basic assembly.
Well—this is it! I&apos;ll go through the fundamentals with two goals in mind:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;provide a genuinely helpful guide for anyone trying to understand assembly, and&lt;/li&gt;
&lt;li&gt;refresh the basics for myself.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I think every programmer should be able to read at least &lt;em&gt;some&lt;/em&gt; assembly.
Very few people need to write assembly regularly, but everyone benefits from having an intuition for how things work beneath all the layers of abstraction.
Modern software stacks hide so much of the machine that we easily forget what actually happens under the hood. This post is meant to be a reminder.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What is assembly?&lt;/h2&gt;
&lt;p&gt;In short: &lt;strong&gt;assembly is a human-readable representation of machine code.&lt;/strong&gt; Here&apos;s a tiny example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;mv a5, a4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mv&lt;/code&gt; means “move,” and &lt;code&gt;a5&lt;/code&gt; and &lt;code&gt;a4&lt;/code&gt; are registers, small pieces of storage inside the CPU that you can access very quickly.
This instruction copies whatever is in a4 into a5.&lt;/p&gt;
&lt;p&gt;Most assembly instructions follow a pattern like this: &lt;strong&gt;instruction, destination, source(s)&lt;/strong&gt;. In general, what you do in assembly boils down to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;loading and storing values in registers and memory&lt;/li&gt;
&lt;li&gt;arithmetic and comparisons&lt;/li&gt;
&lt;li&gt;branches and jumps&lt;/li&gt;
&lt;li&gt;a handful of other simple operations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RISC-V is a great architecture to learn on because it is very minimal: around a few dozen basic integer opcodes in the RV64I base ISA.
By comparison, x86 has thousands of instructions thanks to 40+ years of backward compatibility.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;p&gt;Let&apos;s look at what assembly this small C program produces:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;

int magic(int a, int b) {
    return a + b;
}

int main() {
    int c = magic(3,5);
    printf(&quot;%d\n&quot;, c);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&apos;m compiling this using a &lt;code&gt;riscv64&lt;/code&gt; toolchain, but any C compiler targeting RISC-V will give similar results:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;riscv64-unknown-linux-gnu-gcc -static -o demo demo.c
riscv64-unknown-linux-gnu-objdump -d demo &amp;gt; demo.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&apos;re going to zoom in on the &lt;code&gt;magic&lt;/code&gt; function. Its assembly begins like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;0000000000010406 &amp;lt;magic&amp;gt;:
   10406: 1101                 addi sp, sp, -32
   10408: ec06                 sd   ra, 24(sp)
   1040a: e822                 sd   s0, 16(sp)
   1040c: 1000                 addi s0, sp, 32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s go through this line by line.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;code&gt;addi sp, sp, -32&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;addi&lt;/code&gt; means “add immediate”: add a constant to a register.
So this does:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sp = sp - 32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So whatever &lt;code&gt;sp&lt;/code&gt; was before this function, it&apos;s now that minus 32.
This allocates 32 bytes of stack space for the function. On RISC-V (like most architectures), the stack grows downward in memory, so subtracting from sp makes the stack larger.
The image below illustrates this: where we imagine that the previous value of sp was 32. And that means after this instruction sp is at zero. And we&apos;ve grabbed our stack.&lt;/p&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-09/stack.png&quot; alt=&quot;stack&quot; /&gt;
&lt;/div&gt;
&lt;h3&gt;&lt;code&gt;sd ra, 24(sp)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sd&lt;/code&gt; means “store double-word” (8 bytes on RV64).&lt;br /&gt;
&lt;code&gt;ra&lt;/code&gt; is the return address register: where execution should continue once this function returns. So essentially: “where should I go once I&apos;m done with this function?”. In our case this would be the place where main called this function.&lt;/p&gt;
&lt;p&gt;24(sp) means sp + 24. Therefore the entire thing is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;whatever is in &lt;code&gt;ra&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;store those 8 bytes in sp+24, which is 0+24=24&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And that goes inside our newly allocated 32-byte stack frame, like so:&lt;/p&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-09/ra.png&quot; alt=&quot;ra&quot; /&gt;
&lt;/div&gt;
&lt;h3&gt;&lt;code&gt;sd s0, 16(sp)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This is a similar thing in that it stores something on the stack. But this time it&apos;s the caller&apos;s frame pointer (s0). And we store it at sp + 16.&lt;/p&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-09/frame-pointer.png&quot; alt=&quot;frame-pointer&quot; /&gt;
&lt;/div&gt;
&lt;p&gt;A frame pointer is a stable reference to the start of a function&apos;s stack frame. So for the &lt;code&gt;magic&lt;/code&gt; function this would be 32. But remember now we&apos;re storing the caller&apos;s framepointer. Why?
The reason is that we can&apos;t just override &lt;code&gt;s0&lt;/code&gt; with our frame pointer, otherwise we&apos;d lose whatever was there before. That&apos;s why we:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;store the caller&apos;s frame pointer on the stack (this instruction)&lt;/li&gt;
&lt;li&gt;override &lt;code&gt;s0&lt;/code&gt; with our frame pointer (next instruction)&lt;/li&gt;
&lt;li&gt;and before exiting the function, we restore the caller&apos;s frame pointer into &lt;code&gt;s0&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Also it&apos;s good to note that not all functions need or use a frame pointer, but compilers often generate one because it simplifies debugging and stack unwinding.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;addi s0, sp, 32&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Now we can proceed to the next step, and safely create our own frame pointer. Remember, add immediate means:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s0 = sp + 32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;in our case&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s0 = 0 + 32 = 32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From here onward, the compiler typically accesses local variables relative to s0 rather than sp. You&apos;ll see that soon!&lt;/p&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-09/s0.png&quot; alt=&quot;s0&quot; /&gt;
&lt;/div&gt;
&lt;p&gt;Okay so hopefully that makes sense. It&apos;s a bit of a weird pattern at first, but this is the most common one you&apos;ll see a lot in assembly.
And it&apos;s pretty similar in arm and x86 as well. What we&apos;re doing is setting up the function by making sure we have enough space on the stack,
storing all the references to our caller and we&apos;re basically getting ready to execute the function itself.&lt;/p&gt;
&lt;p&gt;This next part will be a lot easier to wrap your head around, I promise.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Moving the function arguments&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;1040e: 87aa   mv a5, a0
10410: 872e   mv a4, a1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;a0&lt;/code&gt; and &lt;code&gt;a1&lt;/code&gt; hold the first two integer arguments, &lt;code&gt;int a&lt;/code&gt; and &lt;code&gt;int b&lt;/code&gt;. Here the compiler copies them into temporary registers (a5 and a4).
In optimized builds this usually disappears, because the moves are actually unnecessary as we&apos;ll see later.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Saving locals and performing the addition&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;10412: fef42623   sw a5, -20(s0)
10416: 87ba       mv a5, a4
10418: fef42423   sw a5, -24(s0)
1041c: fec42783   lw a5, -20(s0)
10420: 873e       mv a4, a5
10422: fe842783   lw a5, -24(s0)
10426: 9fb9       addw a5, a5, a4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Piece by piece what&apos;s happening is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store whatever is in &lt;code&gt;a5&lt;/code&gt; (which is the function argument &lt;code&gt;int a&lt;/code&gt;) to s0 - 20. &lt;code&gt;sw&lt;/code&gt; means “store word”. Remember s0 was 32. So 32-20=12. And
if you recall &quot;double-word&quot; was 8 bytes, logically a word is then 4 bytes.&lt;/li&gt;
&lt;li&gt;Move the value in &lt;code&gt;a4&lt;/code&gt; (which is &lt;code&gt;int b&lt;/code&gt;) into &lt;code&gt;a5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Store the value in &lt;code&gt;a5&lt;/code&gt;, a.k.a our &lt;code&gt;int b&lt;/code&gt;, to s0 - 24, which is 32-24=8.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
    &lt;img src=&quot;https://erikkaum.com/images/advent-09/adds.png&quot; alt=&quot;adds&quot; /&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lw&lt;/code&gt; means “load word”. So now we&apos;re taking the value at s0-20, back from the stack into a5. Remember this was our &lt;code&gt;int a&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;And now move that value from &lt;code&gt;a5&lt;/code&gt; into &lt;code&gt;a4&lt;/code&gt;. So now &lt;code&gt;int a&lt;/code&gt; is in &lt;code&gt;a4&lt;/code&gt;. Confused? Don&apos;t worry.&lt;/li&gt;
&lt;li&gt;Load from s0-24 into &lt;code&gt;a5&lt;/code&gt;. This is where we stored &lt;code&gt;int b&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;addw&lt;/code&gt; means “add word”. So it adds the 32-bit values in a4 and a5. Finally 🎉&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Phew 😅 now that&apos;s a lot of moving data for just adding two numbers.
If you think this is silly and inefficient, you&apos;re right! We&apos;ll later look at the optimized assembly for this function, so stay tuned!&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Sign-extend and prepare the return value&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;10428: 2781   sext.w a5, a5
1042a: 853e   mv a0, a5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not super important, but &lt;code&gt;sext.w&lt;/code&gt; sign-extends the lower 32 bits to a full 64-bit value, because addw produces a 32-bit result (with RV64 semantics).
And the return value must be in a0, so we move it there.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Restoring the caller&apos;s state&lt;/h3&gt;
&lt;p&gt;Remember the ceremony we did in the beginning with setting stack and frame pointers? Now it&apos;s time to unwind those.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;1042c: 60e2   ld ra, 24(sp)
1042e: 6442   ld s0, 16(sp)
10430: 6105   addi sp, sp, 32
10432: 8082   ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So first we take the return address of the caller, which is at the top of the stack at sp+24.
And we put that into the &lt;code&gt;ra&lt;/code&gt; register, this is where we need to go back, it&apos;s the address of our caller.&lt;/p&gt;
&lt;p&gt;Then we restore the frame pointer to the caller&apos;s. This is so that when we go back into the function that called us,
it can continue to reference its local variables from the correct reference point. Similarly to how we moved &lt;code&gt;int a&lt;/code&gt; and &lt;code&gt;int b&lt;/code&gt; always relative to &lt;code&gt;s0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And then we give back the stack we allocated in the beginning, now the stack pointer is at 0 + 32 = 32, which is what it was when we started this function.&lt;/p&gt;
&lt;p&gt;Finally, return!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The optimized version (-O3)&lt;/h2&gt;
&lt;p&gt;I promised you that I&apos;d show the optimized version as well, here&apos;s the entire function when compiled with &lt;code&gt;-O3&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;000000000001040a &amp;lt;magic&amp;gt;:
   1040a: 9d2d   addw a0, a0, a1
   1040c: 8082   ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it! No frame pointer, no stack usage, nothing extra. The addition happens directly in the return register.&lt;/p&gt;
&lt;p&gt;And the compiler goes even further when we look at the main function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;000000000001030e &amp;lt;main&amp;gt;:
   1030e: 00050537   lui a0,0x50
   10312: 1141       addi sp,sp,-16
   10314: 95850513   addi a0,a0,-1704 # 4f958 &amp;lt;__rseq_flags+0x4&amp;gt;
   10318: 45a1       li a1,8
   1031a: e406       sd ra,8(sp)
   1031c: 143000ef   jal 10c5e &amp;lt;_IO_printf&amp;gt;
   10320: 60a2       ld ra,8(sp)
   10322: 4501       li a0,0
   10324: 0141       addi sp,sp,16
   10326: 8082       ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice anything missing?
There is no call to magic at all.&lt;/p&gt;
&lt;p&gt;The compiler realized that magic(3,5) is always 8 and simply folded the constant, which is the line with &lt;code&gt;li a1, 8&lt;/code&gt;, into the call to printf.
This sort of optimization is extremely common in modern compilers.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Wrap-up&lt;/h2&gt;
&lt;p&gt;Hopefully this tutorial helped you get a better grip on reading assembly, especially how function prologues and epilogues work, how registers are used, and why unoptimized code looks so strangely verbose.&lt;/p&gt;
&lt;p&gt;Happy hacking!&lt;/p&gt;
</content:encoded></item><item><title>Collection of Quotes, part 1</title><link>https://erikkaum.com/blog/advent-08/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-08/</guid><description>The title says it all</description><pubDate>Mon, 08 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Welcome to my eighth post in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; series.&lt;/p&gt;
&lt;p&gt;Today I&apos;ll need to do something rather quick, I don&apos;t have time to write a full blog post. Fortunately, I have an ace up my sleeve.
I&apos;ve been collecting quotes on my phone since I was 15 or 16. Time to go through them and list a few interesting and fun ones.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;If you&apos;re not prepared to be wrong, you will never come up with anything original.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A quote from Sir Ken Robinson&apos;s famous &lt;a href=&quot;https://www.youtube.com/watch?v=iG9CE55wbtY&quot;&gt;TED Talk&lt;/a&gt;. The video was apparently uploaded 18 years ago,
so I wrote down the quote around that time, maybe 16 years ago. The video had a big impact on me back then, it made me confident in
pursuing what I personally wanted to do and not seeking validation from others.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;I&apos;m too fucking busy, and vice versa.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;By Dorothy Parker. I&apos;ve always just found this one really hilarious. That&apos;s why it&apos;s on the list.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;Some pursue happiness, others create it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Origin unknown. It&apos;s a bit cheesy, but hey, I was maybe 17 or 18 when I wrote it down. It was the first time I remember thinking more
deeply about agency. I have the power to change things in my life and create them, rather than needing to chase them.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;Have you ever imagined a world without hypothetical situations?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;By Steven Wright. I just really enjoy paradoxes and things that seem contradictory.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;A person is a person by means of other persons.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I think the origin of this one is unknown. It seems to be a paraphrase of the Ubuntu philosophy. It&apos;s a more poetic version of the common saying &quot;You&apos;re the average of the five people you spend the most time with&quot;.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;Every day above ground is a good day.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Said by the almighty and great thinker Pitbull. And even though I&apos;m not at all a fan of his, this is actually quite well said.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;Pessimists sound smart. Optimists make money.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is by Nat Friedman (or Naval Ravikant, not sure who coined it) and is one of the later additions to my collection. I wrote it down as a reminder to myself not to be Mr. Actually-It&apos;s-More-Complicated-Than-That guy.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;And I&apos;ll leave you with:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In theory, there&apos;s no difference between practice and theory. In practice, there is.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Yogi Berra&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Post Run Reflections</title><link>https://erikkaum.com/blog/advent-07/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-07/</guid><description>Feelings after a 10km race.</description><pubDate>Sun, 07 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Welcome to my seventh post in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; series.&lt;/p&gt;
&lt;p&gt;Today I ran the Paris Saucony 10km race. Since I spent the week preparing for it and it took up most of today&apos;s headspace, I don&apos;t really have the capacity to write about much else. So this post is a bit of an experiment. I&apos;m not entirely sure what it will become.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://erikkaum.com/images/advent-07/race.jpg&quot; alt=&quot;race&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Context&lt;/h3&gt;
&lt;p&gt;I usually run 5-6 times a week, with a few gym sessions in between. I&apos;m not training with any competitive goal in mind; it&apos;s mostly about staying healthy. The last time I ran a race was a marathon before Covid, so it&apos;s been a while. But a few colleagues signed up for the Saucony 10km, and it seemed like a fun challenge, so I joined them.&lt;/p&gt;
&lt;h3&gt;Outcome&lt;/h3&gt;
&lt;p&gt;I ran a personal best on the 10km: 46 minutes and 21 seconds. I&apos;m pretty happy with that.&lt;/p&gt;
&lt;h3&gt;Reflections&lt;/h3&gt;
&lt;p&gt;The first few kilometers went by quickly, and around the third kilometer I settled into a solid pace. I could feel that I was pushing myself, but I was confident I could hold it until the end. Kilometers five to seven were the mentally toughest. I noticed I was slowly dropping speed, so I had to remind myself not to slack. My pace fluctuated more than I would have liked.&lt;/p&gt;
&lt;p&gt;From the eighth kilometer to the finish line, it was simultaneously euphoric and agonizing. It was only a ten-minute window, but it somehow felt like an eternity. The music got louder and the cheering crowd gave me energy, but I was tired and my legs felt heavy. I knew this was the moment to push. I&apos;d made it this far at this pace, and now it was just a mental game to bring it home. Yet it&apos;s so easy to drop the pace without noticing and lose valuable seconds.&lt;/p&gt;
&lt;p&gt;One epiphany I had is that this kind of mental battle is very different from what I&apos;m used to in startups and building software. A race like this is a short burst where you really have to drive yourself to the end. Needless to say, I have nothing else planned for today, and I&apos;m wiped out. Building companies looks more like how I trained for the race: go out for a run almost every day, focus on foundational zone-2 cardio with a few interval sessions, emphasize recovery, and don&apos;t push too hard. Otherwise you can&apos;t sustain it for years.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Startups aren&apos;t won in a ten-minute sprint. They&apos;re won in the quiet, unglamorous miles no one sees.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Summary&lt;/h3&gt;
&lt;p&gt;A friend just &lt;a href=&quot;https://x.com/OliverMolander/status/1997595074318967173&quot;&gt;reminded me&lt;/a&gt; of a quote we used to say in the military:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The only easy day was yesterday&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I love it. The easiest race is the one that&apos;s already over. It definitely feels like that now. I can already feel my brain rewriting the pain, convincing me it wasn&apos;t that bad, even though I know I&apos;m fooling myself. But that self-deception is what makes us do it again. And I will.&lt;/p&gt;
</content:encoded></item><item><title>The Cult of Caring</title><link>https://erikkaum.com/blog/advent-06/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-06/</guid><description>Greatness doesn&apos;t come from talent alone. It comes from caring. Here&apos;s why, and how to put it into practice.</description><pubDate>Sat, 06 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Welcome to my sixth post in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; series.&lt;/p&gt;
&lt;p&gt;The world isn&apos;t divided between geniuses and the rest. It&apos;s divided between people who care and people who don&apos;t. In one of my &lt;a href=&quot;https://x.com/ErikKaum/status/1879528657489629264&quot;&gt;tweets/X posts&lt;/a&gt;, I declared that I&apos;m founding the &lt;strong&gt;Cult of Caring&lt;/strong&gt;. I&apos;d always felt there was something there, but I never really knew how to express it. Grant Slatton&apos;s blog, &lt;a href=&quot;https://grantslatton.com/nobody-cares&quot;&gt;Nobody Cares&lt;/a&gt;, really nailed it and helped me dress my thoughts and feelings in words.&lt;/p&gt;
&lt;p&gt;As Grant lays out, plenty of things in the world just suck. The DMV, Jira, most public transportation, etc. And the thing is, most of these could be excellent with just a little more effort. But they&apos;re not, because nobody cares enough to make that effort.&lt;/p&gt;
&lt;p&gt;To hammer the point home, here&apos;s a wonderful &lt;a href=&quot;https://x.com/ErikKaum/status/1888178740896674028&quot;&gt;quote&lt;/a&gt; from Tobias Lütke, CEO &amp;amp; Co-Founder of Shopify:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The quality at the end of the day is simply a reflection of how much the people who created it gave a shit about the product.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I strongly subscribe to valuing quality. Honestly, I don&apos;t really know why. Maybe it&apos;s guilt, or maybe I simply don&apos;t find it fun to work on things where I can&apos;t go all in and really hone them.&lt;/p&gt;
&lt;p&gt;Anyway, here&apos;s my thesis. It&apos;s biased toward building software, but you can generalize it.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;If you want to be in the top 1% of anything&lt;/strong&gt;, you have to start by caring. Skill, talent, a good education, or other circumstantial advantages give you a decent foundation. But beyond that, you need persistence and caring. Show up every day and let it compound. When you feel like you&apos;re done, ask yourself: Can I add a tiny polish that makes this even 1-2% better? But note, caring and grinding aren&apos;t the same thing, which brings me to my next point.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Caring comes from having honest empathy for the person using your product.&lt;/strong&gt; Grinding doesn&apos;t. &lt;em&gt;Grinding is effort without intention. Caring is intention that shapes effort.&lt;/em&gt; You put in five more minutes because you genuinely want someone to have a good experience. You care how they&apos;ll feel when they use what you built. Simultaneously, caring doesn&apos;t mean perfectionism. It doesn&apos;t mean burnout. It means refusing to ship something you wouldn&apos;t enjoy using yourself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Caring makes you cut the bullshit.&lt;/strong&gt; It forces you to focus on what matters and prioritize brutally. You&apos;re aiming for greatness in one area, so drop the distractions. Do one thing and do it well. The rest won&apos;t matter. Look at Google or now ChatGPT: one box, one input, no distractions.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Now, I&apos;m not perfect. I don&apos;t live by these principles 100% of the time, unfortunately. But this mindset is something I strive toward. Could I have pushed more on this blog post and made it even better? Probably. Use your common sense and keep things in balance and perspective.&lt;/p&gt;
&lt;p&gt;Tomorrow, whatever you do, do it with care. That&apos;s how the Cult of Caring grows.&lt;/p&gt;
</content:encoded></item><item><title>Compiling Zig to RISC-V</title><link>https://erikkaum.com/blog/advent-05/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-05/</guid><description>Continuing hacking the small Milk-V Duo computer</description><pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In my &lt;a href=&quot;https://www.erikkaum.com/blog/advent-02/&quot;&gt;second&lt;/a&gt; &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; post I described the reading group we have where we learn to build performant components used in neural networks to run on a small RISC-V computer.&lt;/p&gt;
&lt;p&gt;So far I&apos;ve just booted it, but now let&apos;s look at actually running some code on it. Most of the examples out there are C, so I thought I&apos;d take on the challenge of using Zig instead to see what it&apos;s like. Turns out, getting some basic things working is super easy.&lt;/p&gt;
&lt;p&gt;For this, I&apos;m using Zig version &lt;code&gt;0.15.2&lt;/code&gt;. If you&apos;re using older versions of Zig, things might look different.&lt;/p&gt;
&lt;p&gt;The first thing is to create a repository and run &lt;code&gt;zig init&lt;/code&gt; to get the basic scaffolding. We&apos;re doing a simple ReLU, and after changing some file names, this is what I have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tree
.
├── build.zig
├── build.zig.zon
└── src
    ├── main.zig
    └── lactose
        ├── nn
        │   └── relu.zig
        └── root.zig

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since we&apos;re using the Milk-V Duo, the name of my neural networks library will naturally be &lt;strong&gt;Lactose&lt;/strong&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;As a quick reminder before we dive in, this is the ReLU function: if the value x is greater than 0 we leave it be, otherwise we set it to zero.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://erikkaum.com/images/advent-05/relu.png&quot; alt=&quot;relu&quot; /&gt;&lt;/p&gt;
&lt;p&gt;So in code the &lt;code&gt;relu.zig&lt;/code&gt; would be:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;pub fn relu(comptime T: type, x: []T) void {
    for (x) |*elem| {
        if (elem.* &amp;lt; 0) elem.* = 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nice!&lt;/p&gt;
&lt;p&gt;Now our &lt;code&gt;root.zig&lt;/code&gt; just exports this function like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;pub const relu = @import(&quot;nn/relu.zig&quot;).relu;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we can write an executable which uses this ReLU function on a slice. Something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;const std = @import(&quot;std&quot;);
const lactose = @import(&quot;lactose&quot;);

pub fn main() !void {
    var prng = std.Random.DefaultPrng.init(42);
    const rand = prng.random();

    var data: [512]f32 = undefined; // 1K floats

    for (&amp;amp;data) |*x| {
        x.* = rand.float(f32);
    }

    lactose.relu(f32, &amp;amp;data);

    std.debug.print(&quot;{any}\n&quot;, .{data});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&apos;ve also made my &lt;code&gt;build.zig&lt;/code&gt; file very convenient so that I can just run &lt;code&gt;zig build&lt;/code&gt; and it does all the correct things for me. Honestly, the build file is probably where I spent most of my time. Since it&apos;s quite long, I&apos;ll just leave it here so you can inspect it later if you want. Note that some of the choices I&apos;ve made are dependent on how I set up my Milk-V Duo. For example, if you&apos;re using libc instead of musl, you&apos;ll need to change that.&lt;/p&gt;

Click to view the full &lt;code&gt;build.zig&lt;/code&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;const std = @import(&quot;std&quot;);

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{
        .default_target = .{
            .cpu_arch = .riscv64,
            .os_tag = .linux,
            .abi = .musl,
        },
    });
    const optimize = b.standardOptimizeOption(.{});
    const mod = b.addModule(&quot;lactose&quot;, .{
        .root_source_file = b.path(&quot;src/lactose/root.zig&quot;),
        .target = target,
        .optimize = optimize,
    });

    const exe = b.addExecutable(.{
        .name = &quot;main&quot;,
        .root_module = b.createModule(.{
            .root_source_file = b.path(&quot;src/main.zig&quot;),
            .target = target,
            .optimize = optimize,
            .imports = &amp;amp;.{
                .{ .name = &quot;lactose&quot;, .module = mod },
            },
        }),
    });

    b.installArtifact(exe);

    const run_step = b.step(&quot;run&quot;, &quot;Run the app&quot;);

    const run_cmd = b.addRunArtifact(exe);
    run_step.dependOn(&amp;amp;run_cmd.step);

    run_cmd.step.dependOn(b.getInstallStep());

    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const mod_tests = b.addTest(.{
        .root_module = mod,
    });

    const run_mod_tests = b.addRunArtifact(mod_tests);

    const exe_tests = b.addTest(.{
        .root_module = exe.root_module,
    });

    const run_exe_tests = b.addRunArtifact(exe_tests);

    const test_step = b.step(&quot;test&quot;, &quot;Run tests&quot;);
    test_step.dependOn(&amp;amp;run_mod_tests.step);
    test_step.dependOn(&amp;amp;run_exe_tests.step);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Anyhow, simply running &lt;code&gt;zig build&lt;/code&gt; gives me the main executable which I can copy into the Milk-V with this command: &lt;code&gt;scp zig-out/bin/main root@192.168.42.1:/root/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;And ssh:ing to the Milk-V, voilà:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://erikkaum.com/images/advent-05/output.png&quot; alt=&quot;output&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now to understand if this is performant or not, one crucial part is to analyze the assembly. Did the compiler generate something clever or naive for us? You can run something like &lt;code&gt;riscv64-elf-objdump -d -C -S zig-out/bin/main&lt;/code&gt; to disassemble the binary. It&apos;s quite messy, so I recommend piping it into a text file.&lt;/p&gt;
&lt;p&gt;If you search for “relu” and hit enter a few times you should be able to find our function looking something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn relu(comptime T: type, x: []T) void {
 1080f04:	715d                addi	sp,sp,-80
 1080f06:	e486                sd	    ra,72(sp)
 1080f08:	e0a2                sd	    s0,64(sp)
 1080f0a:	0880                addi	s0,sp,80
 1080f0c:	852e                mv	    a0,a1
 1080f0e:	fca43023          	sd	    a0,-64(s0)
 1080f12:	fcc43c23          	sd	    a2,-40(s0)
 1080f16:	fcb43823          	sd	    a1,-48(s0)
 1080f1a:	4501                li	    a0,0
 1080f1c:	fea43023            sd	    a0,-32(s0)
 1080f20:	fcc43423          	sd	    a2,-56(s0)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&apos;s the start of the function.&lt;/p&gt;
&lt;p&gt;Now this isn&apos;t optimized at all, we compiled with &lt;code&gt;Debug&lt;/code&gt;. A few things that stand out without going too deep:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The first instruction allocates 80 bytes for us on the stack; we don&apos;t really need that much.&lt;/li&gt;
&lt;li&gt;Second is that there&apos;s a lot of register spill and redundant operations. Meaning that we&apos;re moving and storing a lot of stuff in memory which could have lived in the registers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here for example we&apos;re storing the same thing twice in memory 🤷‍♂️&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1080f12:    fcc43c23    sd a2, -40(s0)
…
1080f20:    fcc43423    sd a2, -56(s0)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Then later on the loop itself is very unoptimized, but I&apos;ll get to that in another post!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This ended up being just a scratch on the surface, but at least it&apos;s an end-to-end example. Hopefully, this helps you if you&apos;re building on RISC-V machines as well. In some upcoming post, I&apos;ll need to go more in-depth into the assembly and how to optimize this ReLU function.&lt;/p&gt;
&lt;p&gt;Happy hacking!&lt;/p&gt;
</content:encoded></item><item><title>People believe in Secrets</title><link>https://erikkaum.com/blog/advent-04/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-04/</guid><description>A revision of Chapter 8 in Zero to One</description><pubDate>Thu, 04 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Chapter 8 of &lt;em&gt;Zero to One&lt;/em&gt; opens with a striking line:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Most people act as if there were no secrets left to find.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you&apos;ve read the book, you know how central “secrets” are to Peter Thiel&apos;s worldview. His famous question: “&lt;em&gt;what valuable company is nobody building?&lt;/em&gt;”, rests on the assumption that hidden truths still exist. Every major breakthrough once began as a secret: the Pythagorean theorem, the abolition of slavery, and countless others.&lt;/p&gt;
&lt;p&gt;But the book was written in 2013-14, and the world has changed dramatically since then. I think it&apos;s time to revisit the chapter. My intuition is that more people today do believe in secrets again.&lt;/p&gt;
&lt;p&gt;Thiel and Masters lay out five reasons why society stopped believing that hard secrets remain. Let&apos;s walk through them one by one.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;1. Geography&lt;/h3&gt;
&lt;p&gt;The first argument is that the age of exploration is over. The world has been mapped; the frontier is closed; “&lt;em&gt;the unknown seems less accessible than ever&lt;/em&gt;”.&lt;/p&gt;
&lt;p&gt;My counterargument today is straightforward and stupidly simple: &lt;strong&gt;space.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Space travel has become cheaper, more frequent, and more culturally relevant since 2014. Reusable rockets land themselves. Mainstream news outlets livestream SpaceX launches. Companies are building data centers in orbit. We&apos;re exploring again.&lt;/p&gt;
&lt;h3&gt;2. Incrementalism&lt;/h3&gt;
&lt;p&gt;Incrementalism is the idea that progress happens step-by-step: grade by grade, rung by rung. In many ways, that still describes how schools, universities, and careers work.&lt;/p&gt;
&lt;p&gt;The rise of AI is the clearest counterpoint. One day, essentially overnight, society gained access to a chat interface that wasn&apos;t an incremental improvement on existing tools; it was a fundamentally new kind of technology. This sort of leap breaks the incrementalist worldview entirely.&lt;/p&gt;
&lt;h3&gt;3. Risk aversion&lt;/h3&gt;
&lt;p&gt;Most people are risk-averse, and that&apos;s probably fine for the human species as a whole.&lt;/p&gt;
&lt;p&gt;But the landscape of risk has changed dramatically in my opinion. Since 2014, we&apos;ve lived through multiple shocks: COVID-19, the war in Ukraine, and a general geopolitical realignment. These events revealed just how fragile many of our institutions are. Systems we assumed were stable turned out to be brittle.&lt;/p&gt;
&lt;p&gt;Risk and disturbances often reopen the search for secrets.&lt;/p&gt;
&lt;h3&gt;4. Complacency&lt;/h3&gt;
&lt;p&gt;This one is probably still mostly accurate. Many people continue to assume that the path to a good life is fixed: get into a respected university, follow a predictable career, secure stability.&lt;/p&gt;
&lt;p&gt;But I&apos;d like to hope that things like AI have shaken the ground a little bit. It&apos;s harder now to believe that the world is fully known or that your future is guaranteed by simply getting into an elite institution.&lt;/p&gt;
&lt;h3&gt;5. Flatness&lt;/h3&gt;
&lt;p&gt;The argument of flatness is that as the world globalizes and becomes a talent pool of similar people, why bother trying to find something new? There&apos;s most likely someone brighter than you already figuring it out.&lt;/p&gt;
&lt;p&gt;Again, this ties to the global situation. I think we&apos;ve somewhat reverted from the path of globalization we expected to have in 2014. There are again Cold War-style dynamics of &quot;us vs them.&quot; Which nation wins the AI race? Who can produce the best hardware?&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;You should believe in secrets&lt;/h2&gt;
&lt;p&gt;Maybe these are reasonable arguments. Or maybe I&apos;m simply optimistic. I hope to see people believing in secrets. And I think you should. Remember the excitement around the alleged room-temperature superconductor LK-99? And self-driving cars: it seems so close, but still far from solved.&lt;/p&gt;
&lt;p&gt;There&apos;s tons of gold to mine for the one who wants to search.&lt;/p&gt;
&lt;p&gt;A pool of infinite opportunities.&lt;/p&gt;
&lt;p&gt;A world filled with secrets to uncover.&lt;/p&gt;
</content:encoded></item><item><title>It&apos;s time</title><link>https://erikkaum.com/blog/advent-03/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-03/</guid><description>A short argument on relentlessly pruning bullshit</description><pubDate>Wed, 03 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Today I&apos;m writing post of Day 3 in the &lt;a href=&quot;https://adventofwriting.com/&quot;&gt;Advent of Writing&lt;/a&gt; challenge. Honestly, I don&apos;t have much to say today, so I&apos;ll keep it short and concise.&lt;/p&gt;
&lt;p&gt;One of the suggested topics was “Write about one key photo, code snippet, graph, etc., and make a story about it.” Instead, I&apos;ll go with a quote from one of Paul Graham&apos;s blogs, &lt;a href=&quot;https://www.paulgraham.com/vb.html&quot;&gt;“Life is Short”&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Having kids showed me how to convert a continuous quantity, time, into discrete quantities. You only get 52 weekends with your 2 year old. If Christmas-as-magic lasts from say ages 3 to 10, you only get to watch your child experience it 8 times. And while it&apos;s impossible to say what is a lot or a little of a continuous quantity like time, 8 is not a lot of something. If you had a handful of 8 peanuts, or a shelf of 8 books to choose from, the quantity would definitely seem limited, no matter what your lifespan was.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To me, the core message of his essay is this: &lt;strong&gt;finiteness sharpens value.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;But not all finiteness is obvious or predictable. Some things, like how many Christmases are left or how many holidays you&apos;ll have with a loved one, can more or less be estimated. Others end abruptly and without warning. You rarely realize, for example, when it&apos;s the last time your child falls asleep on your chest, or the last time a friend lives in the same city. These “last times” only become clear in retrospect.&lt;/p&gt;
&lt;p&gt;We shouldn&apos;t also be discouraged by the fact that valuable moments are finite. There might only be 8 Christmas-as-magic times, but you have all the power in the world to produce more of similar events. Similarly, if everything is precious, nothing is. Don&apos;t be afraid of scarcity.&lt;/p&gt;
&lt;p&gt;We also shouldn&apos;t be discouraged by the fact that valuable moments are finite. There might only be eight “Christmas-as-magic” years, but you have the power to create new rituals just as meaningful. Scarcity heightens appreciation, but it doesn&apos;t have to create anxiety. If everything is precious, nothing is. Don&apos;t be afraid of scarcity.&lt;/p&gt;
&lt;p&gt;Now my question to you is: &lt;strong&gt;what do you want to do today?&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>Hacking on small computers</title><link>https://erikkaum.com/blog/advent-02/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-02/</guid><description>Dabbling with the Milk-V Duo 256M to build neural networks from scratch</description><pubDate>Tue, 02 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Today&apos;s suggested topic for &lt;a href=&quot;https://adventofwriting.lovable.app/&quot;&gt;the Advent of Writing challenge&lt;/a&gt; was “A tech or concept you&apos;ve postponed learning”. Quite perfectly, I&apos;ve been part of a small reading group with a bunch of colleagues focused on understanding computer performance. We decided to ditch the book and instead get practical. We all ordered Milk-V Duo 256M (or equivalent) boards and decided to build up the fundamental blocks of neural networks on this device.&lt;/p&gt;
&lt;p&gt;As it happens, mine arrived in the mail today. So let&apos;s boot it up together. Here&apos;s a small picture of it. It&apos;s literally the size of my thumb.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://erikkaum.com/images/advent-02/device.jpg&quot; alt=&quot;Device&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The Milk-V Duo 256M board is a small, Linux-capable SoC board. So it&apos;s not a microcontroller. This thing actually runs Linux; you can ssh into it and so on. It also has a 1 TOPS-class neural processing unit! 👀&lt;/p&gt;
&lt;p&gt;So, the setup. It was a bit gnarlier than I&apos;d wanted, but in the end, pretty smooth.&lt;/p&gt;
&lt;p&gt;First, we need to flash a micro-SD with the OS. Grab a release from here: &lt;a href=&quot;https://github.com/milkv-duo/duo-buildroot-sdk-v2/releases/&quot;&gt;https://github.com/milkv-duo/duo-buildroot-sdk-v2/releases/&lt;/a&gt;. I used the &lt;code&gt;milkv-duo256m-musl-riscv64-sd_v2.0.1.img.zip&lt;/code&gt;. Unzip it, and let&apos;s move on.
You can also choose to use arm64 with glibc, but in my case we&apos;re learning risc-v in our reading group, so that&apos;s the motivation for my choice.&lt;/p&gt;
&lt;p&gt;Note that this setup is specific to a Mac since that&apos;s what I&apos;m using.
Connect the SD card to your laptop and find it with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;diskutil list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will give something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/dev/disk8 (external, physical)
#: 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which is the SD card.
Then unmount it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;diskutil unmountDisk /dev/disk8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo dd if=milkv-duo256m-musl-riscv64-sd_v2.0.1.img of=/dev/rdisk8 bs=4m status=progress
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and eject the card:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;diskutil eject /dev/disk8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I made the mistake of not properly ejecting this, and it led to some really hard-to-debug situations 😅&lt;/p&gt;
&lt;p&gt;You can also reinsert the card and run &lt;code&gt;diskutil list&lt;/code&gt; again, which should show you something with &lt;code&gt;FDisk_partition_scheme&lt;/code&gt; etc.&lt;/p&gt;
&lt;p&gt;Cool, now put the SD card into the Milk-V and plug it into your computer. Again, a pro tip: make sure your USB-C cable supports data transfer and isn&apos;t just a power cable. Luckily, I keep a box at home where I&apos;ve stored every cable since my teenage years, so I naturally had one.&lt;/p&gt;
&lt;p&gt;After this, you should see a blue light popping up and blinking. If you only see a red light, Linux isn&apos;t booting correctly, and you need to go back to the previous step to figure out what you did wrong.&lt;/p&gt;
&lt;p&gt;The device should be now accessible just through ssh:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh root@192.168.42.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The password is always by default &lt;code&gt;milkv&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you&apos;re successful, you should now see this in your terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[root@milkv-duo]~#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Congratulations 🎉&lt;/p&gt;
&lt;p&gt;Next, let&apos;s run something on it. Since it&apos;s a small device, it&apos;s advised to do all the development on your main machine, cross-compile, and just move the final binary to the Milk-V. Thankfully, the Milk-V Duo crew have made it quite easy to do this. Let&apos;s do a quick example!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Edit:&lt;/strong&gt; Later on I learned that Zig ships with a risc-v toolchain, so you can just do &lt;code&gt;zig build -Dtarget=riscv64-linux-musl&lt;/code&gt;
and it&apos;ll work on the Milk-V.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I cloned the &lt;a href=&quot;https://github.com/milkv-duo/duo-examples&quot;&gt;https://github.com/milkv-duo/duo-examples&lt;/a&gt; repo and used it to get running. Note that these are for x86 Linux, so if you use a Mac like me, you&apos;ll need to set up your environment in Docker. No problem. CD into the duo-example and run something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run --platform linux/amd64 -it -v /Users/youname/…/testing/duo-examples:/work  ubuntu:22.04 /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will create a running docker container and let you mount the examples directory into it, neat.&lt;/p&gt;
&lt;p&gt;From there, you&apos;ll need to run &lt;code&gt;apt-get update&lt;/code&gt; and &lt;code&gt;apt-get install -y wget git make&lt;/code&gt; to have the basics in the docker container.&lt;/p&gt;
&lt;p&gt;Now there&apos;s a nice setup script for you so you don&apos;t need to understand the nuances of the toolchain. The readme is also really good here. But in short run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source envsetup.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And choose 2 and 2 when prompted. Now you should have the correct toolchain installed to be able to cross compile programs for the Milk-V Duo!&lt;/p&gt;
&lt;p&gt;Let&apos;s do the hello world! Run &lt;code&gt;cd hello-world&lt;/code&gt; and run &lt;code&gt;make&lt;/code&gt;. This compiles the &lt;code&gt;helloworld&lt;/code&gt; executable for you, and you can copy it to the Milk-V by running &lt;code&gt;scp helloworld root@192.168.42.1:/root/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Go back, or ssh again, to your Milk-V, and now you should be able to find the program and run it &lt;code&gt;./helloworld&lt;/code&gt;
Which gives you:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[root@milkv-duo]~# ./helloworld
Hello, World!
[root@milkv-duo]~#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&apos;s it 😄👍&lt;/p&gt;
&lt;p&gt;But let&apos;s do something extra. After all, we want to do some neural network stuff on this machine, so why not do a small dot product? In the root of the duo-examples dir I&apos;ve made a directory called &lt;code&gt;dotprod&lt;/code&gt; and wrote this small program:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stddef.h&amp;gt;

static int dot_product(const int *lhs, const int *rhs, size_t len) {
    int sum = 0;
    for (size_t i = 0; i &amp;lt; len; ++i) {
        sum += lhs[i] * rhs[i];
    }
    return sum;
}

int main(void) {
    const int vector_a[] = {1, 3, -2, 4, 5};
    const int vector_b[] = {4, -1, 0, 7, 2};
    const size_t length = sizeof(vector_a) / sizeof(vector_a[0]);

    if (length != sizeof(vector_b) / sizeof(vector_b[0])) {
        fprintf(stderr, &quot;Vector length mismatch\n&quot;);
        return 1;
    }

    int result = dot_product(vector_a, vector_b, length);

    printf(&quot;Dot product: %d\n&quot;, result);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the accompanying &lt;code&gt;Makefile&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-make&quot;&gt;TARGET=dotprod

ifeq (,$(TOOLCHAIN_PREFIX))
$(error TOOLCHAIN_PREFIX is not set)
endif

ifeq (,$(CFLAGS))
$(error CFLAGS is not set)
endif

ifeq (,$(LDFLAGS))
$(error LDFLAGS is not set)
endif

CC = $(TOOLCHAIN_PREFIX)gcc

SOURCE = $(wildcard *.c)
OBJS = $(patsubst %.c,%.o,$(SOURCE))

$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS)

%.o: %.c
	$(CC) $(CFLAGS) -o $@ -c $&amp;lt;

.PHONY: clean
clean:
	@rm *.o -rf
	@rm $(OBJS) -rf
	@rm $(TARGET)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But simply cd:ing to the directory, running &lt;code&gt;make&lt;/code&gt;, and again copying it to the Milk-V, I can do some simple dot products on the device:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://erikkaum.com/images/advent-02/dotprod.png&quot; alt=&quot;dotprod&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Neat! This is it for today. Maybe this is useful for you if want to dive into this stuff as well, maybe not 😄&lt;/p&gt;
</content:encoded></item><item><title>Why I&apos;m joining this challenge</title><link>https://erikkaum.com/blog/advent-01/</link><guid isPermaLink="true">https://erikkaum.com/blog/advent-01/</guid><description>Joining the Advent of Writing challenge to just get it done</description><pubDate>Mon, 01 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;m joining &lt;a href=&quot;https://adventofwriting.lovable.app/&quot;&gt;this challenge&lt;/a&gt; for two reasons: a few deeply held beliefs about communication, and a spontaneous fuck-it-why-not impulse.&lt;/p&gt;
&lt;p&gt;Tl;dr:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Being a really good communicator, written and spoken, is extremely valuable.&lt;/li&gt;
&lt;li&gt;You can become a great communicator simply by practicing.&lt;/li&gt;
&lt;li&gt;I&apos;ve wanted to write more for years but never managed to stick with it. Maybe this time will be different.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Writing is one of those obviously good things everyone knows they should do, yet most people don&apos;t. It&apos;s like living a healthy lifestyle: knowing what to do and actually doing it are two completely different worlds.&lt;/p&gt;
&lt;p&gt;The first thing people overlook is that writing follows a power law: one with massive, uncapped upside and basically no downside. Once you publish something online, it stays there. It gathers views for years. You write once, and the benefits compound like interest.&lt;/p&gt;
&lt;p&gt;Most pieces get a handful of readers, some get a bit more, and a tiny fraction get almost all the attention. Again, a power law. If you know how they work, you now realize now important it is to put your writing out there. If you don&apos;t, you should watch &lt;a href=&quot;https://www.youtube.com/watch?v=HBluLfX2F_k&quot;&gt;this Veritasium video&lt;/a&gt; to get a better intuition of what happens in a game governed by a power law.&lt;/p&gt;
&lt;p&gt;The second overlooked point is how much writing improves your thinking. Even as I&apos;m writing this out, I&apos;m already questioning I arguments made a paragraph earlier. It is ridiculously easy to fool yourself. Writing gives you a third-person perspective  on your thoughts, a way to take distance and scan them. Do it. You&apos;ll refine your ideas, thoughts, and feelings. Btw this is also why writing is so central in successful organizations; Stripe and AWS are famous for it.&lt;/p&gt;
&lt;p&gt;Finally, you just have to get over the embarrassment of sharing your thoughts publicly. A challenge like this helps. It feels awkward, what if I say something stupid? But honestly, who cares? You&apos;re almost always your own harshest critic. So push yourself. Do it. And once it&apos;s done, you&apos;ll be glad you did.&lt;/p&gt;
</content:encoded></item><item><title>Honesty</title><link>https://erikkaum.com/blog/honesty/</link><guid isPermaLink="true">https://erikkaum.com/blog/honesty/</guid><description>Cultivating real honesty is really hard</description><pubDate>Tue, 22 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Let&apos;s do a test.&lt;/p&gt;
&lt;p&gt;This only works if you&apos;re 100% honest with yourself.&lt;/p&gt;
&lt;p&gt;Can you tell your colleague or boss that they&apos;ve done a bad job?&lt;/p&gt;
&lt;p&gt;And I don&apos;t mean the trivial &quot;could you improve on some random trivial thing&quot;. I mean giving really difficult, you-didn&apos;t-do-well-at-your-work type of feedback.&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;Getting &lt;em&gt;&lt;strong&gt;real &amp;amp; honest&lt;/strong&gt;&lt;/em&gt; feedback from your peers isn&apos;t a humanitarian right. It&apos;s a privilege. For others to be able to give you &lt;em&gt;&lt;strong&gt;real &amp;amp; honest&lt;/strong&gt;&lt;/em&gt; feedback, they have to trust you. They have to be 100% sure that you can take it. That you won&apos;t hold a grudge. That you won&apos;t get defensive. That you&apos;re actually able to take in the feedback and are capable of having a civilised discussion.&lt;/p&gt;
&lt;p&gt;Ergo, you can&apos;t force a culture of honesty. Companies that schedule sessions of &quot;let&apos;s all give constructive feedback&quot; miss the point. They are likely to have sessions of people giving feedback in the form of made-up petty items. Not real feedback. Because by giving &amp;amp; receiving &lt;em&gt;&lt;strong&gt;real &amp;amp; honest&lt;/strong&gt;&lt;/em&gt; feedback, you are making yourself vulnerable. Most likely people won&apos;t do that just because HR says so.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Real &amp;amp; honest&lt;/strong&gt;&lt;/em&gt; feedback isn&apos;t some barbaric roast, it&apos;s a deeper form of care for another. Your friend is a reckless driver. You worry the manoeuvres might one day become detrimental. You voice this concern even tough your friend finds this annoying. Because you care.&lt;/p&gt;
&lt;p&gt;It&apos;s a silly example but hopefully the message comes through.&lt;/p&gt;
&lt;p&gt;A leader isn&apos;t someone who gives people orders. A leader is someone who elevates other people to their maximum potential and beyond. Such growth can only be achieved by giving others &lt;em&gt;&lt;strong&gt;real &amp;amp; honest&lt;/strong&gt;&lt;/em&gt; feedback.&lt;/p&gt;
</content:encoded></item><item><title>Focus</title><link>https://erikkaum.com/blog/focus/</link><guid isPermaLink="true">https://erikkaum.com/blog/focus/</guid><description>Staying focused is hard</description><pubDate>Mon, 21 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;People say that startups should have a very narrow focus. Like do one thing - and one thing only - and do it well. People know this but fully internalising it is hard. It should be done:&lt;/p&gt;
&lt;p&gt;Like this.&lt;/p&gt;
</content:encoded></item></channel></rss>