Currently — Math, CS, and AI at NYU Courant

Gagan
Gutta

I build AI systems, mostly applied ML and computer vision, and a few products around them. Sometimes I write code for research, sometimes for startups.

01 / Media

In the press.

02 / Experience

Where I've worked.

01

HerringNet

Research Assistant — Jordaan Labs, UMass Amherst

River herring populations have been declining for decades, and monitoring juvenile emigration is a top federal research priority. The problem: researchers manually review thousands of underwater camera trap images, frame by frame, counting fish. It takes hundreds of hours per field season.

HerringNet replaces that entire workflow. It's a two-stage deep learning pipeline that uses a YOLOv12x model pretrained on 1.9M fish images to detect every fish in a frame, then corrects for double-counting across video using frame-residence-rate methodology. The system processes 2,320+ images at 1.4s each on a consumer CPU, with a 0.72 mean confidence score and zero false positives on debris, leaves, or rocks. No GPU required. Any lab can deploy it out of the box.

Presented at MassURC 2026. Methods paper in preparation targeting Limnology and Oceanography: Methods. Open source.

PythonPyTorchYOLOv8YOLOv12OpenCVSAHIPydanticGradio
2025–2026
02

ContextGrade

Co-Founder & CEO

When a brand's ad shows up next to misinformation, hate speech, or low-quality content, it's a liability. The advertising industry calls this brand safety, and most companies still solve it with blocklists and manual review.

ContextGrade is the infrastructure layer that makes that automatic. We built a content analysis API that scans web pages and scores them for advertising risk across categories like misinformation, toxicity, and brand alignment. The platform started as a consumer browser extension for spotting media bias, then pivoted into B2B after we realized the real problem was on the enterprise side. Currently in talks with Omnicom, one of the five largest advertising holding companies globally, to deliver brand safety analysis at scale.

Started at 17 as a browser extension. Now an enterprise platform.

PythonNLPAPI DesignEnterprise Sales
2023–Present
03 / Personal Projects

Things I've built.

01

Neural Game Engine

A game you play inside a neural network

Train an action-conditioned world model on Doom, then throw the game engine away and drive the model with your keyboard. At play time there is no Doom running: a VQ-VAE compresses each 64x64 frame to an 8x8 grid of tokens, and a transformer predicts the next frame's tokens from the last six frames plus a keypress. Its own output is fed back in as context, so the interesting question is how fast the hallucination drifts. The GIF is a 26M-parameter model tracking the real game from the same keypresses.

Scaled on a pre-registered ladder: 2M, 8M and 26M parameters at the same 2.9B-token budget, scored on the same held-out frames, with three seeds fixing the resolution at 0.08 dB. Headroom over copying the last frame went 58% to 70% to 83%, so the small model was starved, not broken. Getting it playable on a laptop CPU was most of the work: KV caching and MaskGIT parallel decoding take the shipped 2M model from 0.65 to 35 fps with no GPU anywhere in the stack.

I also built a retrieval memory to fight the drift. It does not measurably help at either 2M or 26M, and measuring the key showed why: at a revisit its top match is from the same place only 11% of the time. It ships disabled and the negative result is written up in the README. Public repo with a shipped checkpoint, so python play.py works straight from a clone.

PythonPyTorchVQ-VAETransformersMaskGITVizDoomNumPy
2026
Left: the real VizDoom game. Right: the same keypresses fed to a 26M-parameter world model with no game engine running.
02

baton

A durable job queue, written from scratch in C++20

A single-binary job server in the spirit of Beanstalkd. Apps enqueue work over the Redis wire protocol, workers take it under a lease, and nothing the server has acknowledged is lost, whether it is killed mid-write, a worker dies mid-job, or the power goes. No reply leaves the server until every log record it depends on is fsynced, and group commit lets waiting clients share each fsync: one connection gets 451 durable enqueues per second on my laptop, 256 connections get 30,305. That beats Beanstalkd's fsync-per-job mode 14,148 to 743 jobs/s at 64 connections, though with a single client Beanstalkd wins, 754 to 470.

Most of the work went into proving those guarantees. The log is tested against a simulated file system that can lose, keep or tear unsynced data at any point, a mutation check breaks 29 durability rules one at a time and requires the tests to catch each, and a chaos harness kills the server and workers at random. Its long run killed the server 652 times across 232,682 acknowledged jobs with none lost, and none of 1,157 requests using a dead lease token was accepted, checked by an independent model replaying the server's own log. It also caught two real bugs: leases expiring up to a millisecond early, and a retry ambiguity I fixed by changing the protocol.

Also: a Python SDK with automatic heartbeats and graceful shutdown, snapshots with log compaction, and benchmarks that keep the unflattering rows, like Faktory being faster under load because it doesn't write each job to disk before acknowledging it, or a 126 ms event-loop stall when snapshotting a million live jobs. Single node, no replication. Durable workflows are next and not built yet.

C++20CMakeepoll/kqueueGoogleTestlibFuzzerPythonDockerGitHub Actions
2026
03

attention-emergence

Reproducing and extending an NYU paper on emergent capabilities

A June 2026 NYU paper (arXiv:2606.25010) claims emergent capabilities in transformers appear abruptly at unpredictable times, each jump coinciding with the model suddenly learning a sparse attention pattern. No implementation existed, so I rebuilt its synthetic tasks from scratch in PyTorch and reproduced the core result on a laptop CPU: three identical single-layer transformers, differing only in seed, jump to full accuracy at wildly different times, each jump lining up with attention snapping onto the task's true input bits.

Three transformers, identical except for random seed, training on the same task: accuracy jumps from chance to 100% at three completely different step counts.

The causal test: transplanting a trained model's attention pattern into a plateaued checkpoint at inference recovers nothing (accuracy stays at chance), while forcing the correct pattern in as a bias during training collapses an 800-to-4,200-step plateau to 150 and unlocks a setting where four seeds made zero progress in 20,000 steps unbiased. My first read of this was "the attention pattern is the whole circuit"; after external review I corrected the README to what the data actually shows, that the pattern is the bottleneck for the search and the readout underneath is trainable once it exists, just not free.

Original extension: statistics from only the first 500 steps of training predict when a run will emerge, r = 0.57 across 40 runs (permutation p = 0.008) from a single feature, attention mass already sitting on the correct positions, ahead of anything loss-based. The predictor fails outright on a harder task at the same fixed step count, and recovers only once the window is rescaled to that task's own plateau length, so the forecast clock runs in units of the task's difficulty, not in raw steps.

PyTorchTransformerLensNumPyMatplotlib
2026
04

Puck

A terminal AI assistant that pokes you first

A personal Poke-style assistant that lives in the terminal, remembers things about you, and surfaces reminders unprompted instead of waiting to be asked. The brain is DeepSeek V4-Flash, a few cents a month at personal volume; everything else is local, including a SQLite store that keeps a small curated table of facts separate from a large searchable episodic log, so recall quality comes from curation rather than retrieval volume. Named for Shakespeare's trickster, built the week Cognition bought the original Poke.

Swapping models is three environment variables, not a refactor, so a provider that misbehaves on tool calling is a config change away from being replaced. A mock mode runs the full agent loop with zero API keys and zero installs. The next milestone is read access to Gmail for a morning brief, and the design stance for it is set before the code is: read-only scopes and drafts, never sends, until there's a reason to trust it with anything outbound.

PythonSQLiteLLM tool calling
2026