<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>teenygrad</title>
    <link>https://teenygrad.org/blog</link>
    <description>Design notes, bring-up reports and benchmarks from the teenygrad compiler.</description>
    <language>en-GB</language>
    <lastBuildDate>Tue, 04 Aug 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://teenygrad.org/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Running the parking garage demo using YOLO26</title>
      <link>https://teenygrad.org/blog/parking-garage-demo</link>
      <guid isPermaLink="true">https://teenygrad.org/blog/parking-garage-demo</guid>
      <pubDate>Tue, 04 Aug 2026 00:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Arshad Mahmood]]></dc:creator>
      <description><![CDATA[A step-by-step walkthrough of the vision-rs 0.1.2 parking garage demo — YOLO26 counting occupied bays in the browser, then the same thing running on a Jetson Orin Nano.]]></description>
      <content:encoded><![CDATA[<p>The clip on the <a href="https://teenygrad.org/">landing page</a> is the parking garage demo: a lot viewed from
a fixed camera, every bay outlined, occupancy updating as the feed advances.
This post is how to run it yourself.</p>
<p>It is a useful demo because it is shaped like a real deployment rather than a
benchmark. Two processes, a WebSocket between them, a browser at the end, and a
detection model doing work on every frame. Underneath, YOLO26-N compiled from
Rust to PTX by teenygrad — no Python anywhere in the pipeline.</p>
<p>How it fits together:</p>
<ul>
<li><strong><code>parking-garage-server</code></strong> walks a parking-lot dataset, runs YOLO26 over each
frame, decides which bays are occupied, and broadcasts a snapshot over a
WebSocket every two seconds. Port 3001.</li>
<li><strong><code>parking-garage-webapp</code></strong> serves the Vue page the browser loads. Port 3000.</li>
<li>The page opens a WebSocket back to the server and draws each frame with its
bays overlaid.</li>
</ul>
<p>The first eight steps run everything on a workstation. The last four put it on
a Jetson Orin Nano.</p>
<p>Everything here is against
<a href="https://github.com/teenygrad/vision-rs">vision-rs</a> at branch
<code>release/0.1.2</code>.</p>
<h2>Before you start</h2>
<table>
<thead>
<tr>
<th>Requirement</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ubuntu 24.04+</td>
<td>The only supported build platform right now.</td>
</tr>
<tr>
<td>An NVIDIA GPU</td>
<td>SM_86 or newer for the host path.</td>
</tr>
<tr>
<td>CUDA Toolkit 13.3+</td>
<td>On the host, for the <code>cuda</code> feature and for compiling kernels.</td>
</tr>
<tr>
<td>Rust stable</td>
<td>Via <a href="https://rustup.rs/">rustup</a>.</td>
</tr>
<tr>
<td>The PKLot dataset</td>
<td>Roughly 12 GB. See step 4.</td>
</tr>
</tbody>
</table>
<p>Check the two people most often get wrong:</p>
<pre><code class="language-bash">nvcc --version
nvidia-smi
</code></pre>
<p>If <code>nvcc</code> is missing but <code>nvidia-smi</code> works, you have a driver but no toolkit.
Install the toolkit first — the kernel compiler needs the headers.</p>
<figure><!-- Replace this <div> with: <img src="https://teenygrad.org/media/blog/parking-garage/01-prereqs.png" alt="Terminal showing nvcc and nvidia-smi output"> --><div class="shot">Screenshot: terminal showing <code>nvcc --version</code> reporting 13.3+ and <code>nvidia-smi</code> listing the GPU.</div><figcaption>Fig. 1 — Confirming the toolkit and driver before anything else.</figcaption></figure>
<h2>Step 1 — Clone the repository</h2>
<pre><code class="language-bash">git clone https://github.com/teenygrad/vision-rs
cd vision-rs
git checkout release/0.1.2
</code></pre>
<p>The demo lives in <code>demos/parking_garage/</code>, as a member of the vision-rs
workspace. You will build it from the repository root throughout — <code>cargo teeny</code>
resolves the workspace’s path dependencies and needs the root’s <code>Cross.toml</code>,
so it refuses to run from inside the member directory.</p>
<h2>Step 2 — Install cargo-teeny and the teenyc toolchain</h2>
<p>vision-rs builds on <strong>stable</strong> Rust. The GPU kernels do not — they are compiled
by <code>teenyc</code>, a custom compiler fork, both ahead of time and via JIT at runtime.
You need that binary before anything using the <code>cuda</code> feature will build, and
<code>cuda</code> is what makes this demo do inference at all.</p>
<p>You do not have to build the compiler from source. <code>cargo-teeny</code> fetches a
prebuilt release:</p>
<pre><code class="language-bash">cargo install --git https://github.com/teenygrad/cargo-teeny --branch release/0.1.1
cargo teeny install-toolchain
</code></pre>
<p><strong>Take the <code>--branch</code> seriously.</strong> Packaging this demo needs <code>--package</code> and
<code>--features</code>, and those flags only exist on <code>release/0.1.1</code> — <code>cargo-teeny</code>’s
default branch does not have them yet, and step 10 will fail with an
<code>unexpected argument</code> error if you install from it.</p>
<p><code>install-toolchain</code> downloads the compiler and <code>rustup toolchain link</code>s it as
<code>stable-teenyc-x86_64-unknown-linux-gnu</code>. Verify both halves landed:</p>
<pre><code class="language-bash">rustup toolchain list | grep teeny
rustup run stable-teenyc-x86_64-unknown-linux-gnu teenyc --version
</code></pre>
<p>If the first prints nothing, the link step did not happen, and every later build
fails with a missing-<code>teenyc</code> error rather than anything obviously
toolchain-shaped.</p>
<figure><!-- Replace this <div> with: <img src="https://teenygrad.org/media/blog/parking-garage/02-toolchain.png" alt="Terminal showing the linked teenyc toolchain"> --><div class="shot">Screenshot: <code>rustup toolchain list | grep teeny</code> showing the linked toolchain, and <code>teenyc --version</code> printing a version.</div><figcaption>Fig. 2 — The kernel compiler, installed and linked.</figcaption></figure>
<h2>Step 3 — Configure the environment</h2>
<p>Copy the template and point the cache paths wherever you want models to live:</p>
<pre><code class="language-bash">cp .env.dev .env
</code></pre>
<pre><code class="language-bash">export DATASETS_CACHE_DIR=$HOME/.cache/vision-rs/datasets
export MODELS_CACHE_DIR=$HOME/.cache/vision-rs/models
</code></pre>
<p><code>MODELS_CACHE_DIR</code> is the one this demo requires — the server downloads YOLO26
weights into it on first run. Leave <code>TEENYC_PATH</code> commented out unless you built
your own compiler; the rustup-linked toolchain is auto-detected.</p>
<p>Then, in every shell you build or run from:</p>
<pre><code class="language-bash">source .env
</code></pre>
<p>Forgetting this is the single most common failure. It surfaces as
<code>MODELS_CACHE_DIR not set</code> the moment you pass <code>--model</code>.</p>
<h2>Step 4 — Get the parking lot dataset</h2>
<p>The demo reads <strong>PKLot</strong> — a public dataset of parking lot photographs from
UFPR, each image paired with an XML file marking every bay and whether it is
occupied. Those XML annotations are what the demo scores itself against.</p>
<p>There is no downloader in the repository; fetch PKLot yourself and unpack it.
What matters is the directory shape, because the server walks exactly this
nesting:</p>
<pre><code class="language-text">&lt;dataset_root&gt;/
  &lt;lot&gt;/              # e.g. PUC, UFPR04, UFPR05
    &lt;weather&gt;/        # e.g. Cloudy, Rainy, Sunny
      &lt;date&gt;/         # e.g. 2012-09-11
        &lt;frame&gt;.jpg
        &lt;frame&gt;.xml   # same stem as the .jpg
</code></pre>
<p>A <code>.jpg</code> with no matching <code>.xml</code> is skipped silently, and a lot directory that
yields no usable images is skipped with a warning. If the server reports
<code>no lots found under ...</code>, you have pointed it one level too high or too low —
the root is the directory that <em>contains</em> the lot directories.</p>
<p>The full dataset is around 12 GB. You do not need all of it: a single lot, or
even a single date directory promoted to the right depth, is enough to see the
demo work.</p>
<figure><!-- Replace this <div> with: <img src="https://teenygrad.org/media/blog/parking-garage/03-dataset.png" alt="Directory tree of the PKLot dataset"> --><div class="shot">Screenshot: <code>tree -L 3</code> of the dataset root showing lot / weather / date nesting with paired .jpg and .xml files.</div><figcaption>Fig. 3 — The layout the server expects.</figcaption></figure>
<h2>Step 5 — Build the demo</h2>
<p>Both binaries live in the <code>parking-garage</code> workspace member, and inference is
behind the <code>cuda</code> feature:</p>
<pre><code class="language-bash">source .env
cargo build --release -p parking-garage --features cuda
</code></pre>
<p>The first build compiles the whole teenygrad stack and takes a while. You end up
with two binaries in <code>target/release/</code>: <code>parking-garage-server</code> and
<code>parking-garage-webapp</code>.</p>
<p>Without <code>--features cuda</code> the demo still builds and runs, but <code>--model</code> becomes
an error — you get the dataset’s ground-truth annotations replayed into the
browser and no inference at all. That is a reasonable way to check the plumbing
before involving the GPU.</p>
<figure><!-- Replace this <div> with: <img src="https://teenygrad.org/media/blog/parking-garage/04-build.png" alt="Cargo build completing"> --><div class="shot">Screenshot: <code>cargo build --release -p parking-garage --features cuda</code> finishing with the <code>Finished `release` profile</code> line.</div><figcaption>Fig. 4 — Both binaries built.</figcaption></figure>
<h2>Step 6 — Start the server</h2>
<pre><code class="language-bash">source .env
./target/release/parking-garage-server /path/to/PKLot \
  --model ultralytics/yolo26n
</code></pre>
<p>The dataset root is positional; leave it off and it defaults to
<code>/mnt/data1/datasets/PKLot/PKLot</code>, which is almost certainly not where yours is.
<code>--port</code> moves it off 3001.</p>
<p>Three things happen on first run, in order, and the middle one takes a while:</p>
<ol>
<li><strong>The dataset is scanned</strong> — it prints one line per lot with an image count.</li>
<li><strong>Weights are downloaded</strong> — YOLO26-N as pre-converted safetensors from
Hugging Face, cached under <code>$MODELS_CACHE_DIR</code>. Only once.</li>
<li><strong>Kernels are compiled</strong> — you will see
<code>compiling YOLO26N (nc=80, 640×640) — first run builds kernel cache …</code>.
Every kernel in the network is being compiled for your specific GPU.
Subsequent runs read the cache and start in seconds.</li>
</ol>
<p>Then it settles into a two-second tick, and the API is up on
<code>http://0.0.0.0:3001</code> with the WebSocket at <code>/api/ws</code>. There is a health check
at <code>/api/health</code> if you want to confirm it from another terminal.</p>
<figure><!-- Replace this <div> with: <img src="https://teenygrad.org/media/blog/parking-garage/05-server.png" alt="Server console output"> --><div class="shot">Screenshot: the server console — lot scan results, the weights download, the kernel-compile line, and the first ticks scrolling past.</div><figcaption>Fig. 5 — First run: scan, download, compile, tick.</figcaption></figure>
<h2>Step 7 — Start the webapp and open it</h2>
<p>In a second terminal, from the repository root:</p>
<pre><code class="language-bash">./target/release/parking-garage-webapp
</code></pre>
<p>It listens on <code>0.0.0.0:3000</code> and serves <code>demos/parking_garage/ui/dist</code>. That
path is resolved <strong>relative to your working directory</strong>, so run it from the
repo root or it will serve nothing — this matters again in step 12.</p>
<p>Pass a different listen address as the first argument if 3000 is taken:</p>
<pre><code class="language-bash">./target/release/parking-garage-webapp 127.0.0.1:8080
</code></pre>
<p>Then open <a href="http://localhost:3000">http://localhost:3000</a>.</p>
<p>The page connects to <code>ws://&lt;the host you loaded the page from&gt;:3001/api/ws</code>. On
one machine that resolves itself. If your server is elsewhere, or on a
non-default port, override it with a query parameter:</p>
<pre><code class="language-text">http://localhost:3000/?ws=ws://192.168.1.50:3001/api/ws
</code></pre>
<p>A status dot in the corner tells you where you stand: amber connecting, green
connected with the URL beside it, red on error. It retries on its own, so
starting the webapp before the server is fine.</p>
<p><strong>One caveat:</strong> the page loads Vue from a CDN, so the <em>browser</em> needs internet
access even though nothing else does.</p>
<figure><!-- Replace this <div> with: <img src="https://teenygrad.org/media/blog/parking-garage/06-browser.png" alt="The parking garage demo in the browser"> --><div class="shot">Screenshot: the browser at localhost:3000 — the lot image with every bay outlined, occupancy counts, and the green connected status dot.</div><figcaption>Fig. 6 — The demo, running.</figcaption></figure>
<h2>Step 8 — Read what it is telling you</h2>
<p>The interesting output is in the server’s terminal, not the browser. With
<code>--model</code> set, each tick prints a line per lot:</p>
<pre><code class="language-text">[  UFPR04] 2012-09-11_15_16_58.jpg  gt=22/28 inf=21/28 18.4ms
</code></pre>
<ul>
<li><strong><code>gt</code></strong> — occupied bays according to the dataset’s XML annotation. Ground
truth, hand-labelled.</li>
<li><strong><code>inf</code></strong> — occupied bays according to YOLO26 on this frame.</li>
<li><strong><code>ms</code></strong> — how long that inference took, wall clock.</li>
</ul>
<p>The occupancy rule is deliberately simple: the model detects vehicles — COCO
classes car, bus and truck — and a bay counts as occupied when the centre of any
detected vehicle falls inside it. No tracking, no temporal smoothing, one frame
at a time.</p>
<p>Watching <code>gt</code> and <code>inf</code> track each other frame to frame is the real content of
the demo. They will not always agree, and the disagreements are the interesting
part: a bay half-occluded by the vehicle in front, a motorcycle that is not one
of the three vehicle classes, a car straddling a line so its centre lands in the
neighbouring bay.</p>
<p>Worth being clear about one thing: the boxes drawn in the browser are the
ground-truth annotations, not the detections. The comparison lives in the log.</p>
<figure><!-- Replace this <div> with: <img src="https://teenygrad.org/media/blog/parking-garage/07-output.png" alt="Server log showing gt vs inf counts and latency"> --><div class="shot">Screenshot: several ticks of server log, showing gt/inf counts converging and the per-frame latency.</div><figcaption>Fig. 7 — Ground truth against inference, frame by frame.</figcaption></figure>
<hr>
<p>Everything so far ran on a workstation with a CUDA toolkit and a kernel
compiler installed. The rest puts the same demo on a Jetson Orin Nano that has
neither.</p>
<h2>Step 9 — Set up cross-compilation</h2>
<p>Two one-time additions on the host:</p>
<pre><code class="language-bash">cargo install cross --git https://github.com/cross-rs/cross
rustup target add aarch64-unknown-linux-gnu
</code></pre>
<p>Third, the <strong>aarch64 CUDA libraries</strong> for your board’s JetPack version. These
are not your host’s CUDA — they are the cross-compilation variant, from NVIDIA’s
CUDA Toolkit downloads for your JetPack release. The build mounts them into the
cross container so the binary links against exactly the CUDA your Jetson runs.
JetPack 6.2 puts them at:</p>
<pre><code class="language-text">/usr/local/cuda-12.6/targets/aarch64-linux
</code></pre>
<p>If yours differs, pass <code>--cuda-path &lt;path&gt;</code> on every <code>build</code> and <code>package</code>
command.</p>
<h2>Step 10 — Package both binaries</h2>
<p><code>package</code> cross-compiles for the board <em>and</em> ahead-of-time compiles the GPU
kernels <strong>on the host</strong>, writing them into the bundle. That second half is the
point: there is no <code>teenyc</code> on a Jetson, so a binary that tried to JIT its
kernels there would fail outright.</p>
<p><code>--bin</code> is repeatable, so the server and the webapp go into one bundle sharing a
single kernel cache:</p>
<pre><code class="language-bash">source .env
cargo teeny package \
  --target jetson-orin-nano \
  --package parking-garage \
  --bin parking-garage-server \
  --bin parking-garage-webapp \
  --features cuda \
  --dest ./dist/parking-garage-orin \
  --device cuda \
  --options &quot;capability=sm_87,ptx-version=82&quot;
</code></pre>
<p>Run it from the repository root, not from <code>demos/parking_garage</code> — that is what
<code>--package</code> is for.</p>
<p><code>capability=sm_87</code> is the Orin Nano’s compute capability (Ampere).
<code>ptx-version=82</code> overrides <code>teenyc</code>’s conservative default PTX ISA floor for
<code>sm_87</code>; keep it pinned unless your device is on a materially different CUDA
version.</p>
<p>You get:</p>
<pre><code class="language-text">dist/parking-garage-orin/
  bin/parking-garage-server
  bin/parking-garage-webapp
  cache/            # AOT-compiled GPU kernels, shared by both binaries
  conf/             # provenance: target, device, options, commit, build time
  data/             # empty — you populate this
</code></pre>
<p>The binaries find <code>cache/</code> as their sibling at runtime, so nothing needs setting
on the device for kernels to resolve.</p>
<figure><!-- Replace this <div> with: <img src="https://teenygrad.org/media/blog/parking-garage/08-package.png" alt="Directory tree of the packaged bundle"> --><div class="shot">Screenshot: <code>tree dist/parking-garage-orin</code> showing both binaries under bin/, plus cache/, conf/ and data/.</div><figcaption>Fig. 8 — Two binaries, one pre-compiled kernel cache.</figcaption></figure>
<h2>Step 11 — Deploy to the board</h2>
<pre><code class="language-bash">cargo teeny deploy \
  --package ./dist/parking-garage-orin \
  --host &lt;user&gt;@&lt;orin-host&gt; \
  --dest /home/&lt;user&gt;/parking-garage
</code></pre>
<p>It is <code>rsync -a</code> over SSH. Set up key-based auth first if you would rather not
type a password, though the interactive prompt works. A re-run copies only what
is missing, so it is safe to resume a partial transfer — pass <code>--overwrite</code> to
force a full re-sync after rebuilding. Use <code>--ssh &quot;ssh -p &lt;port&gt;&quot;</code> for a
non-default SSH port.</p>
<p><code>package</code> bundles binaries and kernels, and nothing else. Two things the demo
needs are therefore still on your host, and you copy them yourself:</p>
<pre><code class="language-bash"># The web UI the webapp serves — it looks for ./ui/dist relative to its cwd
rsync -a demos/parking_garage/ui &lt;user&gt;@&lt;orin-host&gt;:/home/&lt;user&gt;/parking-garage/

# Enough of the dataset to tick through
rsync -a /path/to/PKLot/UFPR04 &lt;user&gt;@&lt;orin-host&gt;:/home/&lt;user&gt;/parking-garage/data/PKLot/
</code></pre>
<p>Send one lot rather than all 12 GB unless you have a reason not to. Keep the
lot / weather / date nesting from step 4 intact — the server walks it the same
way on the device.</p>
<h2>Step 12 — Run it on the Jetson</h2>
<pre><code class="language-bash">ssh &lt;user&gt;@&lt;orin-host&gt;
cd /home/&lt;user&gt;/parking-garage
export MODELS_CACHE_DIR=$HOME/.cache/vision-rs/models
./bin/parking-garage-server ./data/PKLot --model ultralytics/yolo26n
</code></pre>
<p>The weights download on first run and need internet on the board. If it has
none, <code>rsync</code> your host’s <code>$MODELS_CACHE_DIR</code> across instead — the layout is
identical.</p>
<p>In a second SSH session, from the same directory, so that <code>ui/dist</code> resolves:</p>
<pre><code class="language-bash">cd /home/&lt;user&gt;/parking-garage
./bin/parking-garage-webapp
</code></pre>
<p>Then, from your laptop, open <code>http://&lt;orin-host&gt;:3000</code>. The page derives its
WebSocket URL from the host you loaded it from, so it finds the server on 3001
without configuration.</p>
<figure><!-- Replace this <div> with: <img src="https://teenygrad.org/media/blog/parking-garage/09-jetson.png" alt="The demo running from the Jetson"> --><div class="shot">Screenshot: the browser on a laptop showing the demo served from the Orin, next to the server's SSH session printing gt/inf/latency.</div><figcaption>Fig. 9 — Same demo, same kernels, running on the board.</figcaption></figure>
<p>Worth pausing on what is not installed on that Jetson. No Rust. No CUDA toolkit.
No Python. No ONNX runtime, no engine builder, no vendor inference server. One
directory holding two binaries, their kernels, a web page and some JPEGs.</p>
<p>The latency figures in the server log will be higher than on your workstation —
that is the point of measuring them there.</p>
<h2>When it goes wrong</h2>
<table>
<thead>
<tr>
<th>Symptom</th>
<th>Cause</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>MODELS_CACHE_DIR not set</code></td>
<td>You did not <code>source .env</code> in this shell.</td>
</tr>
<tr>
<td><code>no lots found under ...</code></td>
<td>Dataset root is at the wrong depth. It must contain the lot directories.</td>
</tr>
<tr>
<td><code>--model requires compiling with --features cuda</code></td>
<td>Built without the feature. Rebuild with <code>--features cuda</code>.</td>
</tr>
<tr>
<td><code>unexpected argument '--package'</code></td>
<td><code>cargo-teeny</code> from the wrong branch — see step 2.</td>
</tr>
<tr>
<td>Build fails looking for <code>teenyc</code></td>
<td><code>install-toolchain</code> did not link. Check <code>rustup toolchain list | grep teeny</code>.</td>
</tr>
<tr>
<td>Server appears to hang on first run</td>
<td>It is compiling every kernel. Let it finish once; the cache makes later runs fast.</td>
</tr>
<tr>
<td>Browser shows an empty page</td>
<td>The webapp was started from the wrong directory and cannot find <code>ui/dist</code>.</td>
</tr>
<tr>
<td>Status dot stays amber</td>
<td>Nothing is listening on 3001, or the page needs an explicit <code>?ws=</code> override.</td>
</tr>
<tr>
<td>Kernel-not-found on the Jetson</td>
<td>The bundle’s <code>cache/</code> was built for different <code>--options</code>. Re-package with the right <code>capability</code>.</td>
</tr>
</tbody>
</table>
<h2>Where to go next</h2>
<p>The <code>yolo26</code> example in the same repository is the lower-level view of the same
model: <code>view</code> for an inference viewer with bounding boxes, <code>verify</code> for mAP over
a validation split, <code>bench</code> for throughput and latency across batch sizes, and
<code>train</code> for training on your own data. <code>--help</code> on any of them is accurate.</p>
<p>If you want the library rather than either demo, <code>vision_rs::detect</code> is the
entry point — an <code>ObjectDetector</code>/<code>DetectorConfig</code> pair that takes JPEG or PNG
bytes and hands back labelled, NMS-filtered detections in about ten lines. The
reference is at
<a href="https://docs.teenygrad.org/api/vision-rs/vision_rs/">docs.teenygrad.org/api/vision-rs</a>.</p>
<p>And if you get stuck, the <a href="https://discord.gg/yuyM3kxZ3">Discord</a> is the fastest
route to an answer.</p>
]]></content:encoded>
    </item>
    <item>
      <title>teenygrad 0.1.1: the first release</title>
      <link>https://teenygrad.org/blog/teenygrad-0-1-1</link>
      <guid isPermaLink="true">https://teenygrad.org/blog/teenygrad-0-1-1</guid>
      <pubDate>Tue, 04 Aug 2026 00:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Arshad Mahmood]]></dc:creator>
      <description><![CDATA[Twelve crates on crates.io, a Rust-to-PTX compiler with no Python on the device, and the argument for building edge AI in a memory-safe language.]]></description>
      <content:encoded><![CDATA[<p>teenygrad 0.1.1 is out. Twelve crates, Apache 2.0, on crates.io. It is the
first version we would suggest anyone actually install — earlier tags exist
only because publishing a workspace of interdependent crates takes a dry run
or two.</p>
<p>What it is: a Rust-native ML compiler and runtime. You write a model and its
kernels in Rust, and <code>teenyc</code> lowers them through MIR, MLIR, Triton and LLVM IR
to PTX. What lands on the device is a binary. There is no interpreter on the
board, no ONNX round-trip at deploy time, no vendor runtime to keep matched
against a driver.</p>
<p>This post covers where the project is going, what is in the release, what is
demonstrably not finished, and — since it is the question we get most — why any
of this is written in Rust.</p>
<h2>The vision</h2>
<p><strong>Empowering the next generation of AI everywhere — uncompromised performance,
unparalleled accessibility.</strong></p>
<p>We want a world where machine learning is genuinely ubiquitous: from the
smallest embedded sensor to the largest distributed cluster, every device able
to run AI efficiently, safely, and without asking permission from a vendor.</p>
<p>Three things follow from that, and they are the standard we hold the codebase
to:</p>
<ul>
<li><strong>Safety meets scalability.</strong> Memory-safe, concurrent and extensible by
design, so that pushing performance does not mean trading away reliability.</li>
<li><strong>Every device is an AI device.</strong> From microcontrollers to data centres,
performance limited by the silicon rather than by legacy software bloat.</li>
<li><strong>Innovation is democratised.</strong> Open development, a permissive licence, and a
backend interface anyone can implement for their own hardware.</li>
</ul>
<p>The gap this addresses is a real one. The frameworks that dominate today are
excellent at what they were built for — large-scale training and serving on
homogeneous infrastructure you control. They are a poor fit for a camera on a
production line, a drone with a power budget, or an inspection rig that has to
run unattended for three years. The closest thing to a good answer has
historically been TensorFlow Lite, and it is written in C.</p>
<h2>The mission</h2>
<p>teenygrad is a high-performance Rust ML training and inference library in the
spirit of PyTorch and tinygrad. Concretely, we are building for:</p>
<ul>
<li><strong>Memory safety and concurrency</strong>, without a runtime tax.</li>
<li><strong>A low memory footprint</strong> — small enough to run on the smallest devices.</li>
<li><strong>Statically typed kernels</strong> — the ML algorithms and the GPU kernels are both
checked by the compiler.</li>
<li><strong>No performance compromises</strong> — hardware-accelerated wherever the hardware
allows it.</li>
<li><strong>Broad hardware support</strong> — not only NVIDIA and AMD.</li>
<li><strong>An extensible backend</strong> — so a vendor or a team can bring up their own
accelerator without forking the framework.</li>
<li><strong><code>no_std</code> compatibility</strong> in the core training and inference path.</li>
<li><strong>Full async support</strong>, including on embedded targets.</li>
<li><strong>Multi-threaded by default.</strong></li>
</ul>
<h2>Why Rust</h2>
<p>This is a design decision with consequences in every direction, so it is worth
arguing properly rather than asserting.</p>
<h3>Memory safety without a garbage collector</h3>
<p>Edge and embedded AI is still overwhelmingly C and C++. That is not an accident
— those languages give you the control over layout, allocation and lifetime
that the domain demands. It is also the reason a large share of the CVEs in
deployed device software are memory-safety failures: use-after-free in a
teardown path, an off-by-one in a buffer of image data, a data race in a
producer/consumer pipeline that only shows up on a busy machine.</p>
<p>The usual escape from that class of bug is a managed runtime, and a managed
runtime is disqualifying here. Garbage collection means non-deterministic
pauses and heap headroom you have to over-provision. If your inspection camera
has a 30 ms frame budget, a collector that occasionally takes 40 ms is not a
performance problem; it is a correctness problem.</p>
<p>Rust removes the entire class of bug at compile time, with no collector, no
runtime and no pauses. <code>Send</code> and <code>Sync</code> extend the same guarantee to
concurrency, which matters more than it first appears: the interesting work in
an inference pipeline is overlapping capture, preprocessing, kernel launches
and postprocessing across cores, and that is exactly the code where data races
are easiest to write and hardest to reproduce.</p>
<h3>The device is not a workstation</h3>
<p><code>teeny-core</code> is <code>no_std</code> by default — <code>std</code> is an opt-in feature, not something
you have to fight your way out of. The tensor types, the computational graph
and the layer definitions are all built on that basis, because a design that
assumes an allocator, a filesystem and threads is very hard to walk back later.</p>
<p>That matters because of what the alternative actually looks like on a device.
The conventional edge path is roughly:</p>
<blockquote>
<p>PyTorch → ONNX export → engine build → vendor runtime → Python dependencies →
device</p>
</blockquote>
<p>Five handoffs, five artifacts, and — more to the point — five version pairs you
have to keep matched in the field. Every dashed line in that chain is a place a
deployment breaks for reasons that have nothing to do with your model. Anyone
who has debugged a mismatch between an ONNX opset, an engine built on a
different minor driver version, and a Python wheel that silently pulled a new
numpy knows the failure mode.</p>
<p>The teenygrad path is one language and one compiler:</p>
<blockquote>
<p>Rust → MIR → MLIR → Triton → LLVM IR → PTX → binary</p>
</blockquote>
<p>One artifact. The interpreter, the packages and the version drift stay on your
workstation, where they are somebody’s problem during CI rather than at 3am on
a customer site.</p>
<h3>Portability without rewriting the model</h3>
<p>The backend is a trait. Kernels are written generically over it, so retargeting
is a change of type parameter rather than a rewrite. This is a real kernel from
<code>teeny-kernels</code>, not a simplification:</p>
<pre><code class="language-rust">#[kernel]
pub fn matmul_forward&lt;T: Triton, D: Float, const BLOCK_K: i32&gt;(
    a_ptr: T::Pointer&lt;D&gt;,
    b_ptr: T::Pointer&lt;D&gt;,
    c_ptr: T::Pointer&lt;D&gt;,
    M: i32,
    N: i32,
    K: i32,
) {
    let pid = T::program_id(Axis::X);
    let n = pid % N;
    let m = pid / N;
    if m &gt;= M {
        return;
    }

    let k_offsets = T::arange(0, BLOCK_K);
    // Block-tiled loads, accumulate, store — plain Rust.
}
</code></pre>
<p><code>T</code> is the backend, <code>D</code> is the dtype, and <code>BLOCK_K</code> is a tile size fixed at
compile time. The pointers are pointers rather than tensors because that keeps
the generated code close to what the GPU actually wants. Nothing in the body
names CUDA. When SPIR-V lands, this kernel does not change.</p>
<p>Cross-compilation gets the same treatment. Targeting a Jetson from an x86
workstation is <code>rustup target add aarch64-unknown-linux-gnu</code> and a build flag —
the same mechanism the rest of the Rust ecosystem uses, not a bespoke second
build system bolted on for the model.</p>
<h3>Static typing all the way down to the GPU</h3>
<p>The kernel above is checked by the Rust compiler. Dtype mismatches, tile sizes
that do not divide, pointer arithmetic that does not typecheck — these are
build failures on your workstation, not a launch failure on a board in a
cabinet you need a ladder to reach. Const generics carry tile sizes into the
type system, so the shapes the compiler reasons about are the shapes the kernel
runs with.</p>
<p>This is the part of the design that pays back most over time. The dynamic,
runtime-checked approach is genuinely better for research iteration, and worse
for everything that happens after the model is chosen.</p>
<h3>No vendor toolchain lock-in</h3>
<p>The lowering path has nothing proprietary in the middle of it. MLIR, Triton and
LLVM are open, the compiler is Apache 2.0, and the backend interface is a
public trait rather than a plugin API we control access to. If you are bringing
up an accelerator, you implement the trait; you do not negotiate.</p>
<p>The practical version of this argument: your CI is <code>cargo test</code>. Your debugger
is the one you already use. Your static analysis is the borrow checker and
clippy, running over the model code and the kernel code alike, because they are
the same language. There is no second toolchain with its own version policy,
its own licence server, and its own opinion about which driver you are allowed
to install.</p>
<h2>What is in 0.1.1</h2>
<p>Twelve crates, all Apache 2.0, all on crates.io:</p>
<table>
<thead>
<tr>
<th>Crate</th>
<th>What it does</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>teeny-core</code></td>
<td>Tensors, computational graph, dtypes, <code>nn</code> layers, device abstraction. <code>no_std</code> by default.</td>
</tr>
<tr>
<td><code>teeny-macros</code></td>
<td>The <code>#[kernel]</code> attribute macro.</td>
</tr>
<tr>
<td><code>teeny-triton</code></td>
<td>The Triton-like kernel DSL that kernels are written against.</td>
</tr>
<tr>
<td><code>teeny-kernels</code></td>
<td>The kernel library — GEMM, convolution, pooling, norms, attention, losses, fused ops.</td>
</tr>
<tr>
<td><code>teeny-compiler</code></td>
<td>Lowers a traced graph to LLVM/MLIR object code, with a CPU path behind the <code>ndarray</code> feature.</td>
</tr>
<tr>
<td><code>teeny-cuda</code></td>
<td>The CUDA backend — driver bindings, device/runtime abstraction, AOT and JIT kernel compilation.</td>
</tr>
<tr>
<td><code>teeny-onnx</code></td>
<td>Parses <code>.onnx</code> protobuf into a teenygrad graph.</td>
</tr>
<tr>
<td><code>teeny-vision</code></td>
<td>Vision model definitions and datasets; MNIST and LeNet-5 today.</td>
</tr>
<tr>
<td><code>teeny-data</code></td>
<td>Dataset loading — download, CSV, <code>safetensors</code>, memory-mapped access.</td>
</tr>
<tr>
<td><code>teeny-quant</code></td>
<td>Weight-only post-training quantization to INT8/INT4/FP8.</td>
</tr>
<tr>
<td><code>teeny-cli</code></td>
<td>Ahead-of-time compilation of a model’s kernels for a given device and config.</td>
</tr>
<tr>
<td><code>teeny-llm</code></td>
<td>An early LLM serving application. The kernel work underneath it is not finished.</td>
</tr>
</tbody>
</table>
<p>The kernel library covers what a vision model needs end to end: GEMM and matmul
with backward passes, <code>conv1d</code>/<code>2d</code>/<code>3d</code>, the pooling family (max, average and
Lp, in one, two and three dimensions), the normalisation family (batch, group,
instance, layer and RMS), FlashAttention-2 generic over <code>Float</code>, the common
losses, elementwise and reduction primitives, and a fused
<code>conv2d + batchnorm + SiLU</code> in both tiled and GEMM formulations.</p>
<p>Around that: dtype-aware kernel dispatch, ahead-of-time kernel compilation with
a kernel cache that defaults next to the deployed package, <code>teenyc</code>
auto-detection through rustup so <code>TEENYC_PATH</code> is optional, and ONNX import
including FlexAttention, LinearAttention and CausalConvWithState.
<code>teeny-quant</code> reads and writes <code>.safetensors</code> following the
<a href="https://github.com/vllm-project/compressed-tensors">compressed-tensors</a>
convention, and has been validated against Ultralytics YOLO checkpoints.</p>
<p>Every publishable crate has documented public API with <code>#![warn(missing_docs)]</code>
enforced, CI runs <code>-D warnings</code> and <code>cargo fmt --check</code>, and the generated docs
are at <a href="https://docs.teenygrad.org">docs.teenygrad.org</a>.</p>
<h3>What works today</h3>
<ul>
<li>The Rust → PTX pipeline, end to end: MIR through to loadable GPU code.</li>
<li>Object detection inference on a Jetson Orin Nano — YOLO26, verified bounding
boxes, running as the parking-garage demo on the landing page.</li>
<li>Training: MNIST and LeNet-5 to 97%.</li>
<li>Cross-compilation from x86 to aarch64 against CUDA 12.6.</li>
</ul>
<h3>Numbers</h3>
<p>On a Jetson Orin Nano Super, relative inference latency, lower is better:</p>
<table>
<thead>
<tr>
<th>Runtime</th>
<th>Relative latency</th>
</tr>
</thead>
<tbody>
<tr>
<td>TensorRT</td>
<td>1.0×</td>
</tr>
<tr>
<td>ONNX Runtime</td>
<td>2.8×</td>
</tr>
<tr>
<td>teenygrad</td>
<td>7.0×</td>
</tr>
</tbody>
</table>
<p>The caveats are load-bearing. This is one model on one board — YOLO26 on a
Jetson Orin Nano Super, MAXN SUPER power mode, JetPack 6.2.2. It is not a claim
about GEMM throughput in general, and it is not a claim about any workload we
have not measured. Reproduction steps are in the repo, and we would rather you
ran them than took our word for it.</p>
<h2>What does not work yet</h2>
<p>The same list is on the <a href="https://teenygrad.org/#status">status section</a> of the landing page, and it
stays there because a roadmap that outruns the code is worse than no roadmap:</p>
<ul>
<li><strong>Detection model training</strong> compiles and runs, but is not yet validated.</li>
<li><strong>One-command build and deploy</strong> to a board is still a manual copy step.</li>
<li><strong>Transformer detection (RF-DETR)</strong> compiles but has not been run.</li>
<li><strong>Language models</strong> are blocked on kernel work in progress.</li>
<li><strong>Non-NVIDIA targets</strong> are not usable yet.</li>
</ul>
<p>If you are evaluating teenygrad for something real today, the honest scope is:
CNN-family vision inference on NVIDIA hardware, including Jetson, with training
that works for small models and is unproven for large ones.</p>
<h2>Roadmap</h2>
<p><strong>Next.</strong> Spacemit K3 — RISC-V with AI extensions — is under active
development and due in Q4 2026. Finishing one-command deploy so that getting a
build onto a board is not a manual copy. Validating detection training rather
than merely running it.</p>
<p><strong>After that.</strong> SPIR-V and ARM GPUs, which together are what “no vendor lock-in”
has to mean in practice rather than in principle. Transformer detection
(RF-DETR) actually run rather than merely compiled. The LLM kernel work that
<code>teeny-llm</code> is waiting on.</p>
<p><strong>Further out.</strong> Static activation quantization, which needs calibration through
the ONNX and compiler path and so depends on work above it. Deeper sparsity
support. Observability and metrics for models in the field. Pushing the <code>no_std</code>
core down onto genuinely small devices — the microcontroller end of “every
device is an AI device” is the part of the vision we have designed for and not
yet delivered.</p>
<p>The ordering is not fixed, and it responds to who is waiting. If the board you
care about is not on the <a href="https://teenygrad.org/hardware">hardware page</a>,
<a href="https://teenygrad.org/#contact">tell us what it is</a> — that is genuinely how the queue gets sorted.</p>
<h2>Getting started</h2>
<pre><code class="language-bash">cargo install cargo-teeny
cargo teeny build --release
</code></pre>
<p>For a Jetson, name the target:</p>
<pre><code class="language-bash">cargo teeny build --target orin-nano --release
</code></pre>
<p>Documentation is at <a href="https://docs.teenygrad.org">docs.teenygrad.org</a>, the source
is on <a href="https://github.com/teenygrad">GitHub</a>, and the
<a href="https://discord.gg/yuyM3kxZ3">Discord</a> is where the conversation happens.
Contributions are welcome from individuals and companies alike;
<code>CONTRIBUTING.md</code> in the repo is the place to start.</p>
<p>The test we hold ourselves to has not changed: if shipping a model requires a
Python runtime on the device, we have not finished the job. 0.1.1 is the first
version where that claim is checkable rather than aspirational. Go and check it.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
