<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Santhosh Kumar on Medium]]></title>
        <description><![CDATA[Stories by Santhosh Kumar on Medium]]></description>
        <link>https://medium.com/@sandskies?source=rss-56e137867d10------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*-fvLOIudvd6CkwSY.</url>
            <title>Stories by Santhosh Kumar on Medium</title>
            <link>https://medium.com/@sandskies?source=rss-56e137867d10------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 11:58:26 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@sandskies/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[What a Warehouse Taught Me About GPU Clusters]]></title>
            <link>https://medium.com/@sandskies/what-a-warehouse-taught-me-about-gpu-clusters-7c748794a36c?source=rss-56e137867d10------2</link>
            <guid isPermaLink="false">https://medium.com/p/7c748794a36c</guid>
            <category><![CDATA[gpu]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[operations-research]]></category>
            <dc:creator><![CDATA[Santhosh Kumar]]></dc:creator>
            <pubDate>Sun, 04 Oct 2026 17:41:48 GMT</pubDate>
            <atom:updated>2026-10-05T19:21:10.597Z</atom:updated>
            <content:encoded><![CDATA[<h3>Building, and honestly benchmarking, a solver-based GPU scheduler and defragmenter</h3><p><em>All code, data pipelines, and results: </em><a href="https://github.com/santhosh-svg-kum/gpudefrag"><em>github.com/santhosh-svg-kum/gpudefrag</em></a></p><p>For years I worked on fulfillment optimization: deciding which warehouse ships which order, how to pack limited capacity without stranding space, and when a slower but globally better plan beats a fast greedy one. GPU infrastructure turns out to be the same problem in a different costume.</p><p>A GPU cluster is a warehouse whose shelves are GPUs and whose orders are jobs. It has the same failure mode: <strong>capacity that exists but can’t be used, because it’s in the wrong shape.</strong></p><p><strong>TL;DR</strong></p><ul><li>Reproduced a strong published baseline (FGD, USENIX ATC ’23) within 1 pp before comparing anything.</li><li>Solver-based batch placement beats FGD by +0.28 pp, but only at the right batch size.</li><li>Demand-driven defragmentation recovers up to +2.6 pp GPU allocation under heavy load; plain bin-packing still wins placement for multi-node gang jobs.</li></ul><figure><img alt="Diagram of two 8-GPU nodes. Before: each node has 4 busy and 4 free GPUs, so a job needing a full 8-GPU node cannot start. After defragmentation: node A is full and node B is entirely free, so the 8-GPU job starts." src="https://cdn-images-1.medium.com/max/1024/1*6DhGHRrMlZY7-sUb002DPg.png" /></figure><p><em>Two 8-GPU nodes, each half busy. Eight GPUs are free, but a job that needs a whole node can’t start. Moving four small jobs once fixes it.</em></p><p>This is <strong>GPU fragmentation</strong>, and on hardware that can cost well over a thousand dollars per GPU per month, every stranded GPU is money you’re paying to keep idle. So I built <strong>gpudefrag</strong>, an open-source scheduler and defragmenter that uses a constraint solver instead of greedy heuristics. Then I spent most of the effort on the part I think matters more than the algorithm: <strong>benchmarking it honestly</strong>, including the cases where it loses.</p><p>This post covers how it works, what it showed, and what surprised me.</p><h3>The supply-chain parallel: heuristics vs. solvers</h3><p>Most Kubernetes-style schedulers place one pod at a time with a scoring heuristic: filter feasible nodes, score them, pick the best. That’s fast and usually good. But it’s greedy: each decision is made without regard to the next ones.</p><p>In supply chain, we rarely accept greedy for capacity decisions. We formulate them as <strong>mixed-integer programs</strong> and let a solver search the joint space, because “best for this order” and “best for today” diverge.</p><p>The usual objection is latency: solvers are slow and schedulers must be fast. That holds less often than you’d think on a GPU fleet. When placement is followed by minutes of node provisioning, driver initialization and model loading, spending a few hundred milliseconds to decide a <em>batch</em> of placements jointly is a good trade, <strong>if</strong> the result is actually better and the solver can never make a decision worse than the heuristic it replaces.</p><p>That “if” is the whole project.</p><h3>Step 0: reproduce a published baseline before claiming to beat it</h3><p>My baseline is <strong>FGD</strong> (Fragmentation Gradient Descent, USENIX ATC ’23), a strong published scheduler for exactly this problem, and a reproducible one. It defines fragmentation as the expected amount of a node’s free GPU that a <em>typical</em> incoming task couldn’t use, and places each pod where fragmentation grows least. The authors released their simulator and the Alibaba production GPU trace it was evaluated on.</p><p>It’s easy to “beat” a baseline you implemented badly, so before writing any solver code I reimplemented six published schedulers (FGD, BestFit, GpuPacking, GpuClustering, DotProd, Random) and built a <strong>calibration gate</strong>: every baseline must reproduce FGD’s <em>published</em> curves within ±1 percentage point, or no comparison gets reported at all.</p><figure><img alt="Dot plot comparing FGD’s published results with gpudefrag’s reimplementations of six GPU schedulers at 130% load; every scheduler matches within 0.7 percentage points, and FGD within 0.09." src="https://cdn-images-1.medium.com/max/1024/1*Lf2Xleq0U2fMiMq8MKfJ9w.png" /></figure><p>All six pass; FGD itself is within 0.1 pp. Getting there taught me things you only learn by matching someone else’s numbers:</p><ul><li><strong>The integer truncation of scores matters.</strong> FGD’s score is int(sigmoid(Δ/1000) × 100). That truncation creates frequent ties.</li><li><strong>The tie-breaker is “lowest node name”</strong>: deterministic, but accidentally a <em>packing</em> rule. Hold that thought; it comes back.</li><li>Exact per-seed reproduction is impossible, so the gate compares seed-means across 10 seeds. It’s statistical, not anecdotal.</li></ul><h3>How gpudefrag places pods</h3><p>Each scheduling session takes a batch of pending pods and:</p><ol><li><strong>Warm-starts from FGD’s own answer.</strong> The FGD placement becomes the solver’s starting point (a “hint”), so the solver begins from a strong published heuristic and only searches for improvements.</li><li><strong>Optimizes FGD’s own objective, jointly over the batch:</strong> first maximize placed GPU work, then minimize total fragmentation.</li><li><strong>Validates every answer in Go</strong> against the real cluster state (device capacity, GPU counts, gang wholeness, topology) and keeps whichever of {solver, FGD} is better by the exact objective. The result is <strong>never worse than FGD on that batch’s objective. </strong>That’s a guarantee about each decision, not about the cluster over time; Result 2 shows the difference. On timeouts, errors or invalid output, it falls back and counts the reason.</li></ol><p>The first version modeled fragmentation directly in OR-Tools CP-SAT, with reified constraints per GPU and per task class. It was correct (it matched brute force on small cases) but <strong>too weak to beat its own warm start in a two-second budget.</strong></p><p>The fix came straight from operations research: a <strong>pattern-based formulation</strong>, the idea behind classic cutting-stock models.<strong> </strong>For each node, Go enumerates the small subsets of batch pods that could land there, with their exact post-placement fragmentation. CP-SAT then solves a small set-packing problem: pick one pattern per node, use each pod at most once. Batches are small, so the patterns are enumerated up front rather than generated on demand (no column generation needed), and it made the solver fast enough to matter.</p><pre>Go: candidates → patterns (exact fragmentation per node subset) → gRPC<br>Python: CP-SAT picks one pattern per node; gangs all-or-nothing; domains respected<br>Go: validate → keep the better of {solver, FGD} → bind</pre><h3>Result 1: it beats FGD on FGD’s own benchmark, at the right batch size</h3><figure><img alt="Chart of GPU allocation at 130% load: gpudefrag with batch size 16 reaches 95.58% versus FGD’s 95.30% (+0.28 points); batch 64 ties FGD, batch 1 is below it, and BestFit trails at 93.19%." src="https://cdn-images-1.medium.com/max/1024/1*AG_oHqxpB4QJtcmm9BXiAw.png" /></figure><p>With batches of 16 pods and a 500 ms budget, gpudefrag allocates <strong>95.58%</strong> of GPUs at 130% offered load vs. FGD’s <strong>95.30%</strong>: <strong>+0.28 pp</strong>, with a paired 95% confidence interval of ±0.12 pp, and it wins on all 10 of 10 seeds. That sounds small until you notice the ceiling: FGD already leaves under 5% on the table.</p><p>The two losing configurations taught me more than the winning one:</p><ul><li><strong>Batch size 64 doesn’t win.</strong> The solver often runs out of time and falls back to FGD. A solver that misses its budget is just an expensive heuristic.</li><li><strong>Batch size 1 <em>loses</em> to FGD.</strong> Optimizing FGD’s exact metric one pod at a time is <em>worse</em> than FGD itself. Remember the tie-breaker: FGD’s truncated scores plus “lowest node name” quietly pack pods onto the same nodes. Removing that accident and optimizing the metric precisely gave up a hidden packing effect the metric doesn’t capture.</li></ul><p><strong>Lesson: the benefit of a solver here comes from deciding a batch jointly, not from computing a heuristic’s metric more precisely.</strong></p><h3>Defragmentation: fix the layout once you know what’s waiting</h3><p>Placement is a bet on the future: you don’t know what arrives next. Defragmentation is different. When a job is <strong>actually blocked</strong> by fragmentation, it computes the <strong>cheapest set of migrations</strong> that frees room for it.</p><p>The design choices that make it safe:</p><ul><li><strong>Demand-driven.</strong> It runs only when a pending job is blocked even though enough GPUs are free in total. No continuous reshuffling.</li><li><strong>Checkpoint-aware cost.</strong> Moving a job costs restart time plus the work lost since its last checkpoint. A job that checkpointed five minutes ago is cheap to move; one that checkpointed ninety minutes ago is expensive. The solver minimizes total lost work.</li><li><strong>Budgets and hysteresis.</strong> At most K moves per tick, the plan must clearly beat its cost, gang members are never moved, and freed capacity is reserved for the blocked job so nothing else grabs it.</li></ul><h3>Result 2: under a time-driven workload, the effect depends on load</h3><p>FGD’s protocol is arrival-only (pods never finish). To test departures, latency and defragmentation, I added a time-driven simulator. It’s an event-driven engine with a single “busy server” scheduler whose decision time counts as virtual time, so a slow solver shows up as queueing delay instead of being hidden.</p><p>The Alibaba trace’s timestamps turned out to be snapshot-like (peak concurrency about 1% of capacity), so this suite uses a <strong>clearly labeled synthetic workload</strong>: real (pod, duration) pairs resampled under Poisson arrivals at a chosen offered load.</p><p>The first runs exposed a real weakness: with spare capacity, gpudefrag <strong>stranded 2–4× more 8-GPU jobs</strong> than FGD. The fragmentation metric weights task types by <em>how often</em> they appear, and 8-GPU jobs are rare by count but huge by capacity. I fixed it with a capacity-weighted objective and a penalty for opening idle nodes. Because seeds 42–46 informed that diagnosis and tuning, the results below come from <strong>held-out seeds 47–51</strong> that were never used for tuning.</p><figure><img alt="Chart of GPU allocation change versus FGD on held-out seeds: gpudefrag placement is -0.56 points at load 0.9 and +0.31 at load 1.1; FGD plus defragmentation is positive at every load, from +1.03 down to +0.07, with wide intervals." src="https://cdn-images-1.medium.com/max/1024/1*rP0t5mN3JHNVZVyctb8pPQ.png" /></figure><ul><li><strong>When overloaded (load 1.1), placement wins: +0.31 pp</strong> allocation, statistically clear.</li><li><strong>With slack (load 0.9), placement loses: −0.56 pp.</strong> FGD’s simple packing is hard to beat when capacity isn’t scarce.</li><li><strong>Defragmentation trends positive</strong> (+0.7 to +1.0 pp at loads 0.9–1.0), but the intervals are wide with five seeds. At load 0.9 its p95 wait cost is negligible; at load 1.0 p95 wait rises by roughly 35–55 s.</li></ul><h3>Result 3: a real gang-training backtest, where defrag shines and placement doesn’t</h3><p>The last suite replays <strong>real submit times and durations</strong> from SenseTime’s <strong>Helios</strong> traces (the Venus cluster: 1,080 GPUs; multi-node “gang” jobs are 58% of GPU-time). It uses the four busiest weeks, synthetic topology domains, and all-or-nothing gangs. The baseline is <strong>Volcano-style bin-packing</strong>, the usual choice for gang workloads.</p><figure><img alt="Chart of GPU allocation change versus Volcano-style bin-packing on the Helios gang-training backtest: adding gpudefrag defragmentation gives +0.01, +1.84 and +2.64 points at 1.0x, 1.25x and 1.5x load, while gpudefrag placement alone is slightly negative." src="https://cdn-images-1.medium.com/max/1024/1*vx-SU8YdiKIU1yum_Mogfg.png" /></figure><ul><li><strong>At normal load, nothing differs.</strong> The cluster is demand-limited.</li><li><strong>As load rises, defragmentation pays off. </strong>Adding gpudefrag’s defragmenter on top of ordinary bin-packing gives <strong>+2.64 ± 2.12 pp</strong> GPU allocation at 1.5× load (paired by week; the CI clears zero). The strongest result wasn’t solver placement at all; it was the defragmenter. On a 1,000-GPU fleet that’s about 26 GPUs’ worth of extra average allocation, without buying hardware.</li><li><strong>gpudefrag’s placement does <em>not</em> beat bin-packing on gangs.</strong> It’s slightly worse (not statistically significant) at every load. The solver isn’t failing: fallbacks were rare. It’s optimizing a metric designed for GPU-<em>sharing</em> workloads, while gangs need whole nodes kept free, which aggressive bin-packing does naturally.</li></ul><h3>What I’d tell someone building this</h3><ol><li><strong>Calibrate your baselines against published numbers first.</strong> It caught real bugs, and it’s the only reason anyone should believe the comparisons.</li><li><strong>Hold out seeds after you tune.</strong> My most flattering results came from the seeds I tuned on.</li><li><strong>Report the negative results.</strong> “Placement doesn’t beat bin-packing on gangs” is as useful to a reader as the wins, and it points to the next experiment.</li><li><strong>Never trust the solver blindly.</strong> Warm-start from the best heuristic, validate every answer, and fall back. “Never worse than the baseline, per decision” is a property you can test, not a hope. Just don’t confuse it with “better over time”; my timed results show those differ.</li><li><strong>In these experiments, defragmentation mattered more than cleverer placement.</strong> Placement guesses the future; defragmentation corrects the past once you know what’s actually waiting.</li></ol><h3>From simulator to control loop</h3><p>The simulator is deliberately not a Kubernetes scheduler. The production version of these ideas is a control loop: observe cluster state and pending demand, detect fragmentation that is actually blocking work, propose a bounded set of migrations, execute them through a reconciler, and verify that observed state converges to desired state. There, idempotency, disruption budgets, topology constraints, rollback and degraded-node handling matter as much as the optimization objective.</p><h3>Limitations</h3><p>These are simulation results. They rank configurations; they don’t predict production numbers exactly. The timed suite is synthetic, the gang suite’s topology is synthetic (Helios doesn’t publish rack layout), and the defragmenter’s restart model is simplified: a fixed restart time, and no eviction grace period.</p><h3>What’s next</h3><ul><li>Warm-starting from the better of FGD and bin-packing, plus a gang-aware objective, to address the gang result.</li><li>A sweep over migration restart cost, to find where defragmentation stops paying off.</li><li>Running it as a real Kubernetes scheduler plugin and defrag controller on a KWOK cluster, next to Karpenter, KEDA and Volcano.</li><li>An elastic mode, where adding and removing nodes is part of the same optimization.</li></ul><h3>Try it</h3><pre>git clone https://github.com/santhosh-svg-kum/gpudefrag &amp;&amp; cd gpudefrag<br>make data     # fetch checksum-pinned public traces<br>make test     # Go + Python tests<br>make calib    # the calibration gate (~10 s)<br>make suite1a  # FGD protocol benchmark</pre><p>Code, benchmarks, and the full results with paired statistics: <a href="https://github.com/santhosh-svg-kum/gpudefrag"><strong>github.com/santhosh-svg-kum/gpudefrag</strong></a>. I’d love feedback from anyone working on GPU scheduling, Kubernetes, or capacity planning.</p><p><em>Thanks to the authors of FGD (Weng et al., USENIX ATC ’23) for releasing their simulator and data, to Alibaba for the openb trace, and to SenseTime for HeliosData. gpudefrag is Apache-2.0.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7c748794a36c" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>