Hi, I'm Teerth.

A twenty year old tech lover who likes turning maths and ideas into solid code. Some of those ideas now live inside the codebases of companies like these.

See the personal work

Quadratic scratch against linear scratch A coral curve rises steeply across the frame while a blue line stays nearly flat, the same shape as scratch memory before and after the fix.
1,281.6x less scratch
Two timings on a short axis A blue axis line carries two dots with the gap between them shaded, the faster timing sitting closer to the left.
1.513x faster
Probe count drawn as nested squares A large mint square fills most of the frame, with a tiny mint square nested near its corner sized to the same ratio as the probe counts before and after.
15,361x fewer probes
Dense comparisons beside sparse comparisons A tight mint grid of dots on the left stands for the old pairwise comparisons, a few scattered mint dots on the right for the pruned count.
65.5x fewer comparisons
Peak memory before and after, stacked Two horizontal blue bars stacked vertically, the lower bar shorter than the upper one, matching the drop in peak memory.
6.42% lower peak
Four collective edges reduced to three Four small green nodes sit in a row joined by three short edges and one longer redundant arc above them; the arc is struck through, the redundant edge removed.
4 edges to 3
Eight scaffolds collapsing into one Eight small violet squares scattered across the frame converge toward one larger violet square, the touched files folding into a single stable scaffold.
23 files
A lower-triangular block schedule A small grid of violet blocks filled only below its diagonal, the causal block schedule the new kernel consumes.
804 lines added
Two outputs collapse to one A point on the left branches to a coral square and a violet square, the two different outputs the same program could emit. On the right a single point leads to one blue square, the stable output after the fix.
5 lines, deterministic
A full spoke of rules gated down to one On the left, four coral spokes radiate solid from a hub, every rule granted cluster wide. On the right, one mint spoke stays solid while three others turn to dashed grey, the rules gated by engine and provider.
145 lines gated
A short chain ending in a pinned failure Five alternating blue and mint dots on a line lead to a coral diamond marking the panic pinned at the end of the chain.
208 SCCs pinned

Merged upstream work

google-deepmind/mujoco #3396

1,281.6x

less scratch memory

island discovery, ntree 4,096

What I fixed, and where.

Eleven landed contributions. Every number below was measured on a named machine against a control, and every one of them sits in a merged diff or on the default branch it targets.

merged into google-deepmind/mujoco_warp #1541

A linear-memory GPU disjoint-set union

I found that the GPU island-discovery kernel carried the same quadratic memory shape onto the device. I replaced it with a linear-memory disjoint-set union.

156.738 us to 101.097 us. Scratch 7,929,856 bytes to 180,224.

  • Python
  • memory O(n^2) to O(n)
How it was checked, 3 ways
  • 99% interval 1.406x to 1.584x
  • faster in 91 of 93 valid pairs, median dense/DSU ratio 2.40
  • exact match against the dense kernels in every block
google-deepmind/mujoco_warp #1541
A pair matrix condensing into a disjoint-set forest An illustration of the mechanism, not of the benchmark scene. A floor of 484 coral cells, one for every pair of 22 trees, builds up, and then every row slides into its diagonal cell, so the matrix condenses into 22 entries, one per tree. Those entries lift off as a forest, and the floor stays behind as a faint ghost of the scratch that is no longer allocated. Each step the forest runs a disjoint-set union. A contact appears as a bundle of threads between two trees, a bead walks from each end up its pointers to a root, and where the two roots differ one root's pointer swings over to the other, so two islands merge and take one colour. Height is depth in the union's tree. Then every pointer snaps straight to its root, and the floor lights every pair of trees that share an island: the answer the pair matrix used to hold, read from 22 entries. The old kernel carried the quadratic shape onto the device and the replacement is linear, so scratch falls from 7,929,856 bytes to 180,224 bytes and the median time from 156.738 microseconds to 101.097, which is 1.513 times faster with a 99 percent interval of 1.406 to 1.584. 22 entries, one per tree every pointer ends at the root of its island in place of 484 cells, one per pair of trees 1.513x faster scratch 7,929,856 B to 180,224 B median 156.738 us to 101.097 us
merged into NVIDIA/NeMo-Relay #481

One scaffold, not one profile per task

I found the learning key included the first user message, so one unchanging system prompt and tool schema fragmented into a new profile per task and observations never accumulated. I keyed it on the stable scaffold instead, with a fingerprint gate that fails closed.

One unchanging scaffold now keys to one profile instead of one per task. 23 files.

  • Rust
  • cache keying
How it was checked, 2 ways
  • 18 test files, 938 lines of tests added
  • the gate fails closed on legacy, missing, shorter and mismatched fingerprints
NVIDIA/NeMo-Relay #481
The same requests, keyed two ways An illustration of the mechanism: the requests, their number and their timing are not measured. Requests stream down from the top. Each is a violet core for the scaffold, the same model, system prompt and tool schema in every one, with a coloured satellite for the first user message, a different colour for every task. At the fork every request is keyed both ways. On the left the old key includes the first user message, so each request takes its message's colour and flies to wherever its key hashes, into a new profile that holds exactly one observation: the one differing field splits the stream into a fan, and no profile ever grows. On the right the new key is the stable scaffold alone, so the message drops out of the key and every request lands in the same profile, a globe of nested shells that fills one dot per observation and rings each time a shell closes. The change is 1,370 lines added and 86 removed across 23 files, with 18 test files and 938 lines of tests, and a fingerprint gate that fails closed. the first user message split the key same model, same system prompt, same tools a new profile per task each holds one observation one scaffold, one profile observations accumulate +1,370/-86 across 23 files 18 test files, 938 lines of tests added
merged into triton-lang/kernels #22

A topology-derived sparse attention kernel

I built a forward-only Triton kernel that schedules causal attention blocks by a topology-derived salience score, instead of scanning every block in the lower triangle.

A topology derived sparse attention kernel. 804 lines added, 17 tests passing.

  • Python, Triton
  • new kernel
How it was checked, 3 ways
  • 17 tests passed
  • correctness checked for 2D and for batched and headed inputs
  • benchmark runner checked in so the table reproduces
triton-lang/kernels #22
Programs walk only the scheduled blocks of a causal attention matrix An illustration of the mechanism: the attention values and the key-block centroids are drawn for the picture, while the schedule and the order of the walk follow the kernel. A causal attention matrix of 16 by 16 blocks lies as a floor in perspective. Only the lower triangle is present, because the upper pairs cannot exist under causal order, and every query is a ridgeline whose height over a key is how strongly it attends there. The schedule gives each query block a short sorted list: the sink column, the local window along the diagonal, and salience picks. In the upper right sit the key-block centroids. Discs grow round them until groups touch and merge, a 0D-persistence-style salience score over key-block centroids, and the two that stay apart longest are picked and become two whole columns of the schedule. Then one program per query block starts at once. Each walks its own list, and the ridges over a block rise only when a program computes it. Between blocks a program jumps straight over the ones it skips, which stay flat and grey. A program glows larger as it accumulates, then writes its output and leaves a coral bead at the end of its row, until the outputs form a chain along the diagonal. 17 tests passed, 5 files changed, 804 lines added. the kernel visits only the scheduled blocks sink column, local window, salience picks salience picked by a topology-derived score 804 lines added 17 tests passed, 5 files changed
merged into google-deepmind/mujoco #3396

Quadratic scratch in island discovery

Island discovery was allocating scratch that grew with the square of the tree count. I rebuilt it to compute the same partition from a disjoint-set forest, so the allocation grows linearly instead.

84,033,568 bytes to 65,568 at ntree 4,096. 1,281.6x less.

ntree = 4,096, scratch 84,033,568 B to 65,568 B
  • C and C++
  • memory O(n^2) to O(n)
How it was checked, 4 ways
  • 1,851 of 1,851 CTest entries passed
  • 300 of 300 shuffled island executions
  • 59 of 59 parity checks over 10,000 generated hypergraphs
  • maximum relative error 0
google-deepmind/mujoco #3396
One cube that stays alone, a block of its copies that the slider grows A single blue cube is the entire scratch allocation the disjoint-set path needs at the chosen tree count. Beside it stands a coral block built from cubes of exactly the same size, one for every allocation of that size the old path would have needed to cover what it actually asked for, and the slider sets how many. Copies pour out of the blue cube, blue as they leave, and set into the block, turning coral as they join it, so every cube in the block is visibly the same size as the one the new path needs. At four thousand and ninety six trees the block holds one thousand two hundred and eighty two cubes against the blue path's one, 84,033,568 bytes against 65,568. The old path's scratch grew with the square of the tree count; the disjoint-set forest grows linearly. new path, whole allocation 65,568 B old path 1,282 copies of it, what the old path asked for 84,033,568 B quadratic before, linear now
merged into tensorflow/tensorflow #124410

A missing back-propagation in transitive reduction

I traced a correctness bug: transitive reduction was documented as full reachability but never back-propagated, so a redundant control edge with a longer alternate path survived the prune. I fixed the back-propagation so the prune removes it.

Four control edges emitted where the unique transitive reduction is three.

  • C++
  • correctness
How it was checked, 2 ways
  • the defect reproduced from the existing VLOG output
  • the unique transitive reduction derived by hand and asserted
tensorflow/tensorflow #124410
Four collectives run in order, and the control edge the prune should have removed An illustration of the change, not a measurement. Four CollectiveReduce ops on one device are drawn as rings, each an all-reduce across a ring of workers whose number is illustrative: the gradient chunks grow on the first lap and carry the sum on the second, until every worker holds the same value. The rings are stacked in the order they run, c4 first and c1 last, and the axis through them is the chain of control edges that serializes them. The edges are created in the order c4 to c3, c4 to c1, c3 to c2, c2 to c1, and the small rings round each op count what the pass believes that op reaches. Before the fix that belief was copied once, when each edge was created, and never back-propagated, so c3 never learns it reaches c1. The prune asks whether c4 reaches c1 some other way, the question stops at c2, and the bypass from c4 to c1 survives outside the tower: four control edges where the unique transitive reduction is three, and the extra edge over-serializes the collectives. After the fix reachability is closed first, climbing from c1, so c3 learns it reaches c1, the same question runs all the way down, and the prune removes the bypass. The collectives then run on three edges. The change is 362 lines added and 26 removed across 4 files. four CollectiveReduce ops, run in order each waits on the one above it, by a control edge c4 c3 c2 c1 4 edges emitted where the reduction is 3 back-propagated, the prune removes 4 → 1 the extra edge over-serializes the collectives +362/-26 across 4 files
merged into dsx-ai-factory/topograph #432

Gating cluster wide RBAC to the rules that apply

The API server ClusterRole rendered the same Kubernetes RBAC rules regardless of engine and provider, so an install that never reached the Kubernetes API, for example provider test with engine slurm, was still granted pods list, nodes get and list, and daemonsets get, cluster wide. I gated each rule on engine.name and provider.name.

145 lines changed across 4 files. pods, nodes and daemonsets access no longer granted cluster wide.

  • YAML, Helm
  • least privilege
How it was checked, 2 ways
  • chart snapshot tests updated across 4 files
  • each rule gated on engine.name and provider.name
dsx-ai-factory/topograph #432
Gates that withdraw a cluster wide reach An illustration of the change, not a map of a real cluster: the hall of nodes, their pods and the layout are illustrative. The cluster is a floor of glass server blades, each holding its pods as small lit blocks, with the agent a daemonset puts on every node sitting on top. In front, on its own disc, a gem stands for the ServiceAccount of a chart install with provider test and engine slurm, an install that never reaches the Kubernetes API. Before the change the ClusterRole rendered the same rules whatever the engine and provider, so coral light arcs from the gem to every pod (pods list), every node (nodes get/list) and every daemonset agent (daemonsets get), cluster wide, and the whole floor takes the coral light. Each rule leaves the gem at its own angle, so the rules form low, middle and high canopies. Then two mint glass walls, engine.name and provider.name, rise around the install. Where their rising edge meets an arc it sparks, and that arc's light pulls back into the gem: pods list first, then nodes get/list, then daemonsets get. The gem turns mint, and for this install none of these rules is granted any more, while the cluster keeps running. The cycle then repeats. With reduced motion, a still shows the walls raised and the old reach as faint traces behind them. +145/-96 across 4 files. granted cluster wide on every install pods list, nodes get/list, daemonsets get gated on engine.name and provider.name provider test with engine slurm: no longer granted +145/-96 across 4 files
merged into google/highway #3244

Pruned collisions by slice structure

I noticed the collision and scan tests were comparing key pairs the slice structure had already ruled out. I pruned by structure so those pairs never reach the compare.

894,081,141 comparisons to 13,643,737 at one million keys. 65.5x.

  • C++
  • pairwise scan pruned by structure, exact rejection
How it was checked, 3 ways
  • atomic counters in the pairwise check, whole builds
  • 6 paired interleaved runs
  • equal duplicate counts on both builds
google/highway #3244
Every pair of keys compared, then only the pairs whose slices overlap An illustration of the mechanism, not a measured build. Along the bottom is a table of slots. Each key sits in its own translucent window, the slice of slots it can land in whatever seed is tried, and a bead marks the slot the current seed gives it. Every arc joins two keys and is taller the further apart their windows are, so the whole dome of arcs is every pair of keys. First the old check sweeps across the keys and compares each key with every key before it: fans of coral arcs fire until the entire dome is lit. Then the light drains from the top down and stops at the height where windows stop overlapping, leaving only the low mint arcs between keys whose slices overlap. The new check sweeps again and compares only those. Two keys on one slot can only happen where windows overlap, so the same amber duplicate pairs are found by both checks. 894,081,141 comparisons at 1,000,000 keys fall to 13,643,737, 65.5 times fewer, with equal duplicate counts on both builds. At 200,000 keys 480,728,084 falls to 18,265,157, 26.3 times fewer. the build compared every pair of keys even pairs the slice structure had ruled out pruned by slice structure before the compare 65.5x fewer comparisons 894,081,141 to 13,643,737 at 1,000,000 keys equal duplicate counts on both builds
merged into google/XNNPACK #10801

Reusing the leading gap in the memory planner

I found the memory planner never considered the free gap sitting before the first live block. I made it reuse that leading gap, which lowers peak workspace.

MobileNet V1 peak 23.862980 to 22.331730 MiB. Workspace 144 MiB to 112 MiB.

  • C++ and C
  • allocation policy, no asymptotic change claimed
How it was checked, 3 ways
  • memory-planner-test 23 of 23 passed
  • 30 randomized baseline and candidate MobileNet pairs
  • identical output hashes through the public API
google/XNNPACK #10801
The value that fits in the leading gap, stacked on top and then set into it An illustration of the planner's decision, drawn from a small generated memory plan rather than the MobileNet plan itself. Time runs left to right in operator steps and memory offset runs upward from the floor of the arena. Every value is a translucent slab spanning the steps it is live for, at the offset the planner gave it, and the arena has to be as tall as its highest slab for the whole run, so its lid is the peak. The wall of values builds itself first. Below the first live block there is a free cave. The old planner never looked there: its search starts at the first live block, sweeps upward, finds no gap, and stacks the amber value on top, so the lid rises from 112 to 144 across every step although the value is live for only a few of them. The fix looks from the floor, finds the cave, and the value is carried down and set into it, so the lid comes back down to 112 and the old peak is left as a faint coral mark. Every other value is placed identically both ways. Measured on MobileNet V1, FP32 peak workspace fell from 23.862980 to 22.331730 MiB, 6.42 percent lower, and FP16 from 21.255882 to 19.916039 MiB, 6.30 percent lower. Through the public API the workspace fell from 144 MiB to 112 MiB with identical output hashes. memory-planner-test passed 23 of 23, and 30 randomized paired runs reproduced the deltas exactly. a free gap sat below the first live block the old planner never looked there, so the arena grew reused: workspace 144 MiB to 112 MiB FP32 MobileNet V1 peak 6.42% lower, FP16 6.30% lower memory-planner-test 23/23, 30 paired runs
landed in facebook/pyrefly #4180

Pinning a 208-module recheck propagation panic

An export change can consume the 100 epoch incremental budget, produce another export change during forced invalidation, and reach commit with that change still pending. I generated a chain of 208 two-module strongly connected components inside one Rust test and recorded the resulting Transaction has uncommitted changes failure with should_panic, so the bug is pinned rather than argued about.

208 two-module SCCs chained in one Rust test. The failure is pinned at the end with should_panic.

  • Rust
  • regression pinned
How it was checked, 2 ways
  • a chain of 208 two-module SCCs generated inside one Rust test
  • the failure pinned with should_panic so it cannot return quietly
facebook/pyrefly #4180
An export change runs a chain of 208 components, and another reaches commit after the door shuts An illustration of the reproducer, not a measurement. All 208 two-module strongly connected components of the chain generated inside one Rust test are drawn, wound into a tapering coil that descends to commit at its foot. Each component is a pair of beads, one per module, bound by a short thick link, and a thin link joins it to the next. One export change enters at the top and propagates down the coil, turning each component mint as it passes. A violet hoop around the chain marks where the 100 epoch incremental budget runs out; the change consumes the budget reaching it, and a membrane closes across the hoop as the door shuts. Forced invalidation then produces another export change, in coral, on the far side of the door, and it runs the rest of the coil and reaches commit with that change still pending. That arrival bursts, the Transaction has uncommitted changes failure, and a swell runs back up every component still pending. A ring closes around the burst and holds it, as the test pins the failure with should_panic so it cannot return quietly. Then the coil eases back to blue and the next change enters. one export change, 208 two-module SCCs the incremental budget runs out at epoch 100 another change reaches commit, still pending Transaction has uncommitted changes pinned with should_panic so it cannot return quietly
merged into google-deepmind/mujoco #3450

An inverted point-id table for convex hulls

Convex hull graph construction was scanning every pair of points to find a match. I built an inverted point-id table that finds it directly instead.

16 lines added. 15,361x fewer probes at V=40,962. Cold load 134.400 ms to 71.163 ms.

  • C++
  • probes O(V^2) to O(V)
How it was checked, 2 ways
  • probe counters compiled into the loop on main
  • graph checksums identical at every size
google-deepmind/mujoco #3450
A storm of probes against a single stroke, building the same convex hull An illustration of the change, drawn on a small convex hull of a point cloud that turns slowly in three dimensions. Building the hull's graph means finding, for every corner of every face, which hull vertex it is. The old way scans the vertex list from the top for every corner until it finds a match: coral rays spray from a corner of the face to every entry tested, the first entries in the list glow hot because every scan starts there, and in its time only a few faces are finished. The new way writes an inverted point id table once, each vertex ringing as it is written, and then every corner is one mint stroke straight to its vertex, so the whole hull closes in the same time. Both builds are drawn at the same number of probes per second, and either way the graph is identical, as its checksums were at every size. Faces are coloured by the order they were built, mint first to violet last. The scan costs three V squared minus six V probes and the table eight V minus twelve. At ten thousand two hundred and forty two vertices the scan measured three hundred and fourteen million, six hundred and forty three thousand, three hundred and seventeen probes; at forty thousand nine hundred and sixty two vertices the table needs fifteen thousand three hundred and sixty one times fewer. A cold load falls from 134.400 milliseconds to 71.163 milliseconds. before: scan every point to find a match V = 10,242 -> 314,643,317 probes after: one jump, straight to the vertex 15,361x fewer probes at V=40,962 Cold load 134.400 ms to 71.163 ms
landed in openxla/xla #46539

Deterministic GPU codegen in reduction grouping

GroupDisjointReductions iterated a hash set to choose which value survived each union-find merge, so the order of grouped_roots depended on hash iteration order and the same XLA program could emit different GPU code between runs. I made the order deterministic in five lines.

Five lines. Same program, two outputs before, one stable output after. XLA compiles for JAX and TensorFlow.

  • C++
  • determinism
How it was checked, 2 ways
  • five lines across two files
  • the same program now emits the same GPU code on every run
openxla/xla #46539
Ten runs of the same program split between two outputs before, and stack into one after An illustration of the mechanism, not a measurement. The same XLA program is drawn twice, before the change on the left and after it on the right. Its reduction roots lie on a floor in groups, shown as soft regions, and that grouping is identical in both panels and in every run. What varied was the order of the groups: GroupDisjointReductions iterated a hash set to choose which value survived each union-find merge, so the order of grouped_roots depended on hash iteration order. Each run lays a thread through the roots in the order its groups came out and ends at the GPU code that order emits, and each run is laid one level above the last, so ten runs build a stack. Blue is code X and coral is code Y. On the left the ten runs split between two orders, so the stack is two tangled routes and each output column collects only some of the runs. On the right the order is fixed, every run follows the one below it, the threads join into one continuous folded curtain, and code X collects all ten runs. Five lines across two files made the order deterministic. ten runs of the same program before: hash set order after: fixed order code Y code X code X, every run GroupDisjointReductions: five lines, two files XLA compiles for JAX and TensorFlow

Every number on this page is in a merged diff under my GitHub username.

I also build my own.

I keep fourteen public repositories. Some are topology, some are numerics, some are systems. What they share is that each one ends in a number I can measure.

See the personal work

Walk through it in 3D.

The same work, as an interactive 3D portfolio.

Open the 3D portfolio