<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: fab2s</title>
    <description>The latest articles on Forem by fab2s (@fab2s).</description>
    <link>https://forem.com/fab2s</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2587317%2F5c0af3c0-8a66-4069-b585-0016e46fb193.png</url>
      <title>Forem: fab2s</title>
      <link>https://forem.com/fab2s</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/fab2s"/>
    <language>en</language>
    <item>
      <title>I wanted to describe a network, not assemble it: FlowBuilder in flodl</title>
      <dc:creator>fab2s</dc:creator>
      <pubDate>Mon, 04 May 2026 19:56:48 +0000</pubDate>
      <link>https://forem.com/fab2s/i-wanted-to-describe-a-network-not-assemble-it-flowbuilder-in-flodl-4a74</link>
      <guid>https://forem.com/fab2s/i-wanted-to-describe-a-network-not-assemble-it-flowbuilder-in-flodl-4a74</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/fab2s/why-i-built-a-rust-deep-learning-framework-and-what-i-got-wrong-twice-first-3747"&gt;Last post&lt;/a&gt; was why. This one is what it looks like.&lt;/p&gt;

&lt;p&gt;The thing I said at the end of last post was: &lt;em&gt;with flodl I don't rewrite when I pivot. I add or remove a graph member.&lt;/em&gt; This post is the primitive that makes that sentence true. Meet FlowBuilder. It's a declarative graph DSL for neural networks, and it's the API I'd find hardest to give up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;By my third Python pivot, the wiring code outweighed the model. Freezing submodules, loading partial checkpoints, rerouting a tensor through a newly-inserted path, unfreezing for a finetune: each of these was three to ten lines of procedural glue that had nothing to do with the architecture. The model was in there somewhere, but finding it meant reading past everything else first.&lt;/p&gt;

&lt;p&gt;What I wanted was simple. I wanted to &lt;em&gt;describe&lt;/em&gt; the network. What's its structure? What's tagged? What's frozen? What loads from where? And then I wanted the framework to handle the wiring.&lt;/p&gt;

&lt;p&gt;Procedurally assembling a network from module instances and class hierarchies is fine when the shape stays stable. Mine wasn't. A shape that pivots every two days and nests frozen subgraphs inside other frozen subgraphs doesn't want to be a script. It wants to be a graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  What FlowBuilder looks like
&lt;/h2&gt;

&lt;p&gt;Here's a small model with a tagged hidden activation and a residual connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;FlowBuilder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;784&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.through&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GELU&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.through&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;LayerNorm&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.also&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"residual"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.through&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Top to bottom, the architecture is visible in the code. No construction state to hold in your head; the structure &lt;em&gt;is&lt;/em&gt; the text.&lt;/p&gt;

&lt;p&gt;The method names carry the intent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;from(...)&lt;/code&gt; starts the flow with an entry module.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;through(...)&lt;/code&gt; chains a module in series. Stream in, stream out.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tag("name")&lt;/code&gt; marks the current stream position for later reference: observation, freezing, checkpoint loading.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;also(Linear::new(...))&lt;/code&gt; adds a residual: &lt;code&gt;output = stream + module(stream)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;build()&lt;/code&gt; finalizes and validates. Unmerged streams and cycles surface as errors at build time, not at forward time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's more in the vocabulary. &lt;code&gt;fork&lt;/code&gt; for side-branches that don't disturb the main stream. &lt;code&gt;split&lt;/code&gt; with &lt;code&gt;merge(MergeOp::Add)&lt;/code&gt; or &lt;code&gt;merge(MergeOp::Mean)&lt;/code&gt; for parallel branches that recombine. &lt;code&gt;switch&lt;/code&gt; and &lt;code&gt;gate&lt;/code&gt; for routing. &lt;code&gt;loop_body&lt;/code&gt; for iteration. &lt;code&gt;map&lt;/code&gt; for applying a body across slices or tagged collections. The thing I care about is that the builder stays flat. A complex graph is more lines, not more indentation.&lt;/p&gt;

&lt;p&gt;When I pivot a shape, I add or remove lines. The rest of the build doesn't move.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graph renders itself
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;Graph&lt;/code&gt; carries enough structural information to draw itself. One method call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="nf"&gt;.svg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"model.svg"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That writes an SVG with modules as nodes and stream connections as edges. Tags appear annotated on the nodes they marked, and parallel-execution levels are grouped as clusters. For training loops, &lt;code&gt;svg_with_profile(...)&lt;/code&gt; colors nodes by measured forward-pass time so the hot path is visible instead of guessed at.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fczqdezan6ta9sgi1o9wn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fczqdezan6ta9sgi1o9wn.png" alt="FlowBuilder SVG output: tagged residual block" width="307" height="833"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Layout runs through graphviz. It works well up to a few dozen nodes. Past that the visualization gets dense and I start squinting. That's one of the edges I'm still working on. The DOT output is available raw for people who want to pipe it through their own tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Graph trees
&lt;/h2&gt;

&lt;p&gt;Here's the part I think matters most, because no other DL framework I know has it at this shape.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;Graph&lt;/code&gt; implements &lt;code&gt;Module&lt;/code&gt;. That means a Graph can be fed into another FlowBuilder anywhere a module is expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;encoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;FlowBuilder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.through&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GELU&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"encoder"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;FlowBuilder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.through&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.label("encoder")&lt;/code&gt; registers the inner graph as a named child of the outer. Once composed, the inner graph's structure is addressable from the outer scope via label paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="nf"&gt;.freeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"encoder"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                              &lt;span class="c1"&gt;// freeze every parameter in the subgraph&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="nf"&gt;.load_subgraph_checkpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"encoder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// load weights into just that subgraph&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="nf"&gt;.tagged_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"encoder.hidden"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// read the tagged activation across the boundary&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="nf"&gt;.subgraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"encoder"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                            &lt;span class="c1"&gt;// recover the child Graph&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nesting composes. An encoder inside a classifier inside a multi-head pipeline gives you paths like &lt;code&gt;head.classifier.encoder.hidden&lt;/code&gt;, and everything addressable by label keeps working at any depth. Freeze, thaw, load, observe, swap.&lt;/p&gt;

&lt;p&gt;This is the primitive FBRL needed. A trained letter reader is a frozen Graph inside the word reader. A trained word reader is a frozen Graph inside the line reader. Each level addressable by name, each level's checkpoint loadable independently, gradients cleanly blocked at the frozen boundary.&lt;/p&gt;

&lt;p&gt;Transfer learning. Multi-phase pretraining. Anywhere you're stitching trained components into larger architectures and you want the composition to stay legible in a year when you come back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it isn't yet
&lt;/h2&gt;

&lt;p&gt;One real edge: when the wiring is wrong, the error messages are functional but not great. If you merge two branches with mismatched shapes, you get a shape-mismatch error. You do not get told which branch of which split produced the offender. For a short graph you eyeball it. For a deep graph you add prints. I have a list of places where the errors need to carry more structural context back out to the user. That's next-round work.&lt;/p&gt;

&lt;p&gt;I flag this one because it's the rough edge I touch most often. The shape of the API is right. The ergonomics of the error path is what needs sharpening.&lt;/p&gt;

&lt;h2&gt;
  
  
  What hooked me (again)
&lt;/h2&gt;

&lt;p&gt;I started flodl because FBRL needed a composition primitive Python didn't give me cleanly. By the time FlowBuilder was working, I'd noticed I was solving a framework problem I cared about for its own sake. Ergonomics pulled me in first.&lt;/p&gt;

&lt;p&gt;Then performance. Then distributed training. Then convergence under heterogeneous compute.&lt;/p&gt;

&lt;p&gt;This is the part of the journey I didn't expect. I'll walk through the rest of it post by post.&lt;/p&gt;

&lt;p&gt;Previous post: &lt;a href="https://dev.to/fab2s/why-i-built-a-rust-deep-learning-framework-and-what-i-got-wrong-twice-first-3747"&gt;Why I built a Rust deep learning framework (and what I got wrong twice first)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next post: how flodl actually benchmarks against PyTorch on real architectures, and what the libtorch FFI bet from last post actually buys you.&lt;/p&gt;

&lt;p&gt;flodl: &lt;a href="https://flodl.dev" rel="noopener noreferrer"&gt;flodl.dev&lt;/a&gt; · &lt;a href="https://github.com/flodl-labs/flodl" rel="noopener noreferrer"&gt;github.com/flodl-labs/flodl&lt;/a&gt; · &lt;a href="https://x.com/flodl_dev" rel="noopener noreferrer"&gt;@flodl_dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>machinelearning</category>
      <category>showdev</category>
      <category>deeplearning</category>
    </item>
    <item>
      <title>Why I built a Rust deep learning framework (and what I got wrong twice first)</title>
      <dc:creator>fab2s</dc:creator>
      <pubDate>Thu, 23 Apr 2026 17:03:40 +0000</pubDate>
      <link>https://forem.com/fab2s/why-i-built-a-rust-deep-learning-framework-and-what-i-got-wrong-twice-first-3747</link>
      <guid>https://forem.com/fab2s/why-i-built-a-rust-deep-learning-framework-and-what-i-got-wrong-twice-first-3747</guid>
      <description>&lt;p&gt;The Python script that made me give up. It had more boilerplate for freezing and re-composing submodules than it did for the actual model. I'd already pivoted three times. The next pivot was going to cost another rewrite. That's the day I decided to build this in Rust.&lt;/p&gt;

&lt;p&gt;I should say upfront: before this, I had never trained a deep learning model. The path to here was unusual. A theoretical physics degree (with a PhD grant I turned down), then a long detour through documentary film and independent cinema, then self-taught software engineering and twenty years architecting scalable data systems through the startup-scaling era. Pattern recognition across domains is the thing I trust most in my own thinking. A wide-focus lens.&lt;/p&gt;

&lt;p&gt;What I'm building &lt;a href="https://flodl.dev" rel="noopener noreferrer"&gt;flodl&lt;/a&gt; for is research called FBRL, Feedback Recursive Loops. It started as a hobby to explore the field. For now the shape is what matters. Mixing modalities. Feedback loops: images read, classified, and reproduced to force honest attention. Composition that goes letter, then word, then line, then paragraph. Each level frozen and used as an oracle for the level above. The vision was always nested, always partially-frozen, always graph-shaped. That shape is what broke Python for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Python dead end
&lt;/h2&gt;

&lt;p&gt;I started with sound and vision mixed together. That failed. I reframed to a foveal approach: the model reads letters by attention, and at each step it also tries to reproduce what it read. The reproduction forces more abstract latent representations. The letter model is the most developed part of this work so far.&lt;/p&gt;

&lt;p&gt;Composition was always the next step. Read a letter. Then read a word that reuses the frozen letter reader. Then read a line that reuses both. Each level adds capability while the frozen levels below stay reliable.&lt;/p&gt;

&lt;p&gt;Before I even tried to write the composition code, the Python script for the letter model alone had exploded in complexity. Every architectural pivot, and there were many, added more boilerplate than it removed. Per-op dispatch overhead was biting on top of that, especially in the recurrent attention loop where I was making hundreds of small kernel calls per training step.&lt;/p&gt;

&lt;p&gt;What I actually wanted was a way to &lt;em&gt;describe&lt;/em&gt; the network. Not procedurally assemble it from instances and module hierarchies. Describe it. What's its structure? What's tagged? What's frozen? What loads from what checkpoint? Looking at the kind of object I needed to express, the answer was obvious. This is a graph, not a script.&lt;/p&gt;

&lt;p&gt;I had a prior on this. A few years back I'd built a graph library for a data-processing project, and I knew what the graph-shaped headspace felt like. The itch was familiar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two false starts
&lt;/h2&gt;

&lt;p&gt;Python failed me first, Go second. The project was called goDL. It taught me more than it shipped.&lt;/p&gt;

&lt;p&gt;The thing that killed it was the GC trap. Garbage collection plus GPU memory ownership do not compose. You end up with tensors the garbage collector thinks are dead but that the GPU is still using, or the inverse, tensors the GPU is done with but that the GC won't clean for another generation. You can layer manual lifetime management on top, but at that point you've reinvented Rust ownership in a language that's actively fighting you about it.&lt;/p&gt;

&lt;p&gt;So: Rust. Then flodl.&lt;/p&gt;

&lt;h2&gt;
  
  
  The libtorch FFI bet
&lt;/h2&gt;

&lt;p&gt;Several Rust deep learning frameworks exist already. Pure-Rust GPU paths are real and the people building them are doing serious work. None of them, when I started, gave me what I wanted.&lt;/p&gt;

&lt;p&gt;The bet I made for flodl was libtorch FFI through a thin C++ shim. It is not pure Rust. It does not run on every backend. It inherits libtorch's memory footprint. What I get in exchange is CUDA parity today. NCCL today. Tensor Cores today. Mixed precision, CUDA Graphs, fused multi-tensor optimizers. Not in six months. Now.&lt;/p&gt;

&lt;p&gt;I came to programming through the startup-scaling era, where the daily question was how to architect systems that hold up at volume. Shipping production-grade systems is what I do know. The deep learning math I'm still learning as I build. When I chose libtorch FFI, it was the shipping instinct talking: stand on a battle-tested C++ library, and you get production-grade performance today rather than hoping a pure-Rust kernel path catches up over the next few release cycles.&lt;/p&gt;

&lt;p&gt;That bet pays off in measurable ways. There's a benchmark suite that compares flodl to PyTorch on ten architectures, more on that later. For now the point is just: libtorch FFI was a deliberate choice with known costs, not a shortcut.&lt;/p&gt;

&lt;h2&gt;
  
  
  What flodl looks like today
&lt;/h2&gt;

&lt;p&gt;flodl has, today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tensor and autograd backed by libtorch. 100+ tensor operations, 90+ differentiable.&lt;/li&gt;
&lt;li&gt;nn modules at rough PyTorch parity: activations, losses, optimizers (SGD, Adam, AdamW, RMSprop, RAdam, NAdam, with fused CUDA kernels), conv (1d/2d/3d, transposed), recurrent (GRU and LSTM cells and full sequences), attention, normalization (batch, layer, group, instance, RMS), pooling, dropout variants, embedding.&lt;/li&gt;
&lt;li&gt;A declarative graph DSL called FlowBuilder, with visualization.&lt;/li&gt;
&lt;li&gt;Hierarchical graph composition with selective freeze and partial checkpoint loading. The thing the FBRL composition shape needs.&lt;/li&gt;
&lt;li&gt;Transparent multi-GPU training on heterogeneous hardware. One training loop, one or N GPUs.&lt;/li&gt;
&lt;li&gt;Production niceties: mixed precision, CUDA Graphs, fused optimizers, async data prefetch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Saying that as a list does not do the work. The point is that flodl has crossed the line from "can I express what I want" to "does the framework hold up under real workloads I care about." It does.&lt;/p&gt;

&lt;p&gt;flodl is two months old. The velocity comes from AI collaboration on implementation. I'm the architect and the decision-maker. The bets, the API shape, the priorities, the truth-discipline this series will hold itself to: those are mine. Many of the lines of code: not. The pace is what AI partnership makes possible, and I'll be explicit about that throughout the series.&lt;/p&gt;

&lt;h2&gt;
  
  
  What hooked me
&lt;/h2&gt;

&lt;p&gt;I started flodl because FBRL needed it. Building it pulled me into questions I didn't expect: ergonomics, performance, distributed training, convergence under heterogeneous compute. That is the rest of this series. Walking through what got built and why.&lt;/p&gt;

&lt;p&gt;For now the simplest thing I can say about why flodl exists is the thing that has stayed true through three Python rewrites, one failed Go attempt, and the Rust work that became flodl:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;With flodl I don't rewrite when I pivot. I add or remove a graph member.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next post: &lt;a href="https://dev.to/fab2s/i-wanted-to-describe-a-network-not-assemble-it-flowbuilder-in-flodl-4a74"&gt;FlowBuilder, and what a declarative graph DSL actually looks like in Rust&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;flodl: &lt;a href="https://flodl.dev" rel="noopener noreferrer"&gt;flodl.dev&lt;/a&gt; · &lt;a href="https://flodl.dev" rel="noopener noreferrer"&gt;github.com/fab2s/flodl&lt;/a&gt; · &lt;a href="https://x.com/flodl_dev" rel="noopener noreferrer"&gt;@flodl_dev&lt;/a&gt;    &lt;/p&gt;

</description>
      <category>rust</category>
      <category>machinelearning</category>
      <category>showdev</category>
      <category>deeplearning</category>
    </item>
  </channel>
</rss>
