DeepSeek-V3: the recipe, and the interconnect that shaped it
Almost every choice in this model is downstream of one number on a spec sheet.
Download deepseek-v3_from_scratch.py — everything below runs from it. NumPy only, and nothing in it is a benchmark: it counts bytes and simulates schedules.
I read this report the way everyone did — looking for the trick. There isn't one. There are about eight, and they all point the same direction, and I couldn't see the direction until I noticed which GPUs they were on.
US export controls meant DeepSeek trained on H800s. Same chip as an H100 for arithmetic. Roughly half the NVLink — the chip-to-chip link inside a server. For a mixture-of-experts model that is the cruellest possible cut, because routing tokens to experts is an all-to-all, and all-to-all is exactly what a narrow link punishes.
You've built this system before
You have a service that fans a request out to backends and merges the replies. On a fast local network you write it the obvious way:
def handle(req):
picks = router.pick(req, k=8) # 8 backends, wherever they live
return merge([call(b, req) for b in picks]) # ship the payload to each
Then you move it to a link a third the speed, and it falls over. Not because the backends got slower — they didn't — but because the payload now spends longer on the wire than in the handler. You already know the three fixes, because you've shipped all of them:
def handle(req):
picks = router.pick(req, k=8, max_racks=4) # 1. cap the fan-out
payload = compress(req) # 2. shrink every byte
fut = send_async(picks, payload) # 3. overlap the wire with work
local = do_other_work()
return merge(fut.result(), local)
That is DeepSeek-V3's engineering half, in three lines. Cap the fan-out is node-limited routing. Shrink every byte is FP8. Overlap the wire with work is DualPipe. Everything else is detail about how far below the abstraction layer they were willing to go to make those three actually happen.
| Term | Means | Ops analogy |
|---|---|---|
| All-to-all | Every GPU sends a slice of its tokens to every other GPU holding a chosen expert, then gets results back | A shuffle stage. The bit of your MapReduce that actually costs money |
| NVLink | The link between GPUs inside one server. 900 GB/s on H100, 400 GB/s on H800 | The rack-local network |
| InfiniBand | The link between servers. 400 Gbps = 50 GB/s per GPU | Cross-datacentre. Same request, ten times the latency budget |
| Expert parallelism | Experts live on different GPUs, so tokens travel to reach them | Sharding by key. The shuffle is the price |
| Bubble | A pipeline stage sitting idle waiting for its neighbour | A worker blocked on IO with nothing queued behind it |
| PTX | NVIDIA's assembly-level layer, one below CUDA C | Dropping to inline asm because the compiler won't do what you want |
The one formula
Not a loss function. The formula that matters here is the one that decides whether your GPUs are computing or waiting:
Read it as pipeline stages:
| Step | For one MoE layer | What it does |
|---|---|---|
FLOPs | k × 3 × 2 × dmodel × dffn per token | Top-8 SwiGLU experts. Three matrices each, two flops per multiply-add |
bytes | nodes × dmodel × (1 + 2) on IB, k × dmodel × (1 + 2) on NVLink | One copy per destination node out, results back. FP8 out, BF16 back |
throughput | 1979 TFLOPS FP8 | Identical on H100 and H800. This is the number export controls did not touch |
bandwidth | 900 or 400 GB/s | The whole paper. This is the number they did touch |
ratio < 1 | compute-bound | Your tensor cores set the clock. Make the model bigger |
ratio > 1 | comm-bound | Your wire sets the clock. Every design decision changes |
The constraint, counted
DeepSeek-V3 is 671B total parameters, 37B active per token, 61 layers, dmodel 7168, 256 routed experts plus one shared, top-8 routing, expert hidden size 2048. Take one GPU with a micro-batch of 4096 tokens through one MoE layer and count.
The bandwidth numbers below are published specifications, not something I measured, and the "times" are bytes divided by those specifications. This is the same kind of counting the FlashAttention page does for HBM traffic — a roofline sketch, not a profile.
Don't take my word for it. Here's the actual output:
tokens on this GPU : 4096
d_model : 7168, top-8 routed experts, capped at 4 nodes
expert FLOPs per token : 704.6 MFLOP
expert FLOPs, this batch : 2.89 TFLOP
InfiniBand bytes (out+in): 352.3 MB (4 node copies x (1B dispatch + 2B combine))
NVLink bytes (out+in): 704.6 MB (8 expert copies inside the nodes)
arithmetic intensity of this layer:
2,731 FLOP per byte moved
machine balance (FLOP per byte the link can feed, FP8 compute):
H100 NVLink 900 GB/s: 2,199
H800 NVLink 400 GB/s: 4,948
InfiniBand 50 GB/s: 39,580
2,731 FLOP per byte is what this layer offers. 2,199 is what an H100's NVLink can feed. 4,948 is what an H800's can. The layer sits between them. That is the entire story, and here it is as time:
machine compute NVLink leg comm/compute verdict
H100 900 GB/s 1.46ms 0.78ms 0.54x COMPUTE-bound
H800 400 GB/s 1.46ms 1.76ms 1.21x COMM-bound
same layer, same arithmetic, one number on a spec sheet halved,
and the bottleneck moves from the tensor cores to the wire.
And the cross-node leg, which export controls did not touch, is worse than either — which is why the node cap exists at all:
the cross-node leg, which the export cut did NOT touch:
H100: IB 7.05ms + NVLink 0.78ms = 5.37x compute (9.0x the NVLink leg)
H800: IB 7.05ms + NVLink 1.76ms = 6.04x compute (4.0x the NVLink leg)
The report is explicit that it tuned the node cap so the InfiniBand and NVLink legs can be overlapped, citing an effective NVLink-to-IB bandwidth ratio of about 3.2:1. That implies an achieved NVLink figure nearer 160 GB/s than 400. Treat my 900 and 400 as ceilings nobody reaches; the shape of the table is the point, not the decimals.
Auxiliary-loss-free balancing: the neatest idea in the paper
A router that plays favourites is the hot-shard problem, and the MoE page fixes it the standard way: add an auxiliary loss that penalises uneven routing. It works. It also adds a gradient — one that is, by construction, not the task's gradient. You are asking the router to be balanced instead of being right.
V3 replaces it with something an SRE would recognise immediately: a thermostat.
selection_score = affinity + bias # bias affects WHO IS PICKED
gate = affinity # and never the loss, so grad(bias) = 0
bias[expert] -= gamma * sign(load[expert] - mean_load) # after each step
Overloaded expert, bias down. Starved expert, bias up. Fixed step, sign only. It moves the selection and leaves the gradient alone. My script sets up a toy world where token demand is Zipf-skewed — so the routing the model wants is genuinely imbalanced, and balance and quality really do fight.
true expert demand (Zipf, the routing the model actually wants):
[162 90 73 49 43 35 27 33]
max/min expert occupancy, by step (1.0 = perfectly balanced):
step none aux-loss bias (V3)
0 11.2 11.2 11.2
10 8.7 3.8 8.1
20 6.6 3.2 2.0
40 6.1 3.0 2.2
59 6.1 3.0 2.1
final per-expert token counts (512 tokens, top-1):
none : [165 89 74 51 41 33 27 32]
aux-loss: [121 92 71 56 48 41 42 41]
bias : [80 53 60 74 62 50 43 90]
V3 bias vector: [-3. -1.8 1.2 1.2 3. 3.6 3.6 3. ]
Both balance. Now the number that motivates the whole idea — the gradient the balancing mechanism contributes to the router's weights, and how well the router still picks the right expert once you take the bias back off:
gradient norm contributed by the balancing term:
aux-loss : 0.434286 <- fights the routing the model wanted
bias (V3): 0.000000 <- exactly zero, by construction
router accuracy vs the true best expert (bias removed):
no balancing : 0.9590
aux-loss : 0.8848
bias (V3) : 0.9590
Better balance and no accuracy loss. Turn the auxiliary weight up and you get the failure the paper is built to avoid — you buy balance you already had, with quality:
BREAK IT: crank the auxiliary loss weight to alpha=6.0
balance max/min : 3.1 (aux 1.50: 3.0, bias: 2.1)
router accuracy : 0.7324 <- balanced, and worse
balancing grad : 1.323678
The thermostat has its own failure mode, and it's the one you'd guess:
BREAK IT: bias update rate gamma=8.0 instead of 0.6
max/min occupancy, last 8 steps:
[202. 81.8 224. 202. 63.6 153. 63.4 153. ]
vs gamma=0.6: [2. 1.6 2. 1.6 2. 1.6 1.3 2.1]
final bias vector: [144. 144. 144. 144. 144. 152. 144. 144.]
Node-limited routing: capping the fan-out
A token wants its top-8 experts wherever they live. Left alone it can touch eight different servers, and every extra server is another InfiniBand copy of the same 7168-wide vector across the slowest link in the machine. The cap: score each node by the sum of its experts' affinities, keep the best M nodes, and pick the top-8 only from those. V3 uses M = 4.
256 experts over 32 nodes (8 experts per node), top-8, 4096 tokens
uncapped: mean 6.652 nodes/token, max 8
capped : mean 3.748 nodes/token, max 4 (M=4)
InfiniBand bytes, uncapped: 585.9 MB (11.72 ms at 50 GB/s)
InfiniBand bytes, capped : 330.1 MB (6.60 ms at 50 GB/s)
saved: 43.7%
what the cap costs -- experts still reachable per token:
uncapped : 256 of 256 (100.0%)
capped : 32 of 256 (12.5%)
mean overlap with the uncapped choice: 3.62 of 8 experts
It is not free, and the script prints exactly how unfree: a capped token gets 3.62 of the 8 experts it would otherwise have chosen, and only an eighth of the expert pool is reachable at all. You are trading model freedom for wire time. Sweep the cap and watch the trade:
M=2: 2.000 nodes/token, 176.1 MB IB, overlap 2.57/8
M=4: 3.748 nodes/token, 330.1 MB IB, overlap 3.62/8
M=8: 5.330 nodes/token, 469.5 MB IB, overlap 5.00/8
M=32: 6.652 nodes/token, 585.9 MB IB, overlap 8.00/8
M is not chosen to minimise traffic — M=2 would do that. It is chosen so the IB leg and the NVLink leg are close enough in size to hide underneath each other, given that 3.2:1 ratio. The cap and the overlap are one design, not two.
FP8: the other half of the byte reduction
Halving the fan-out halves the copies. Halving the payload halves what each copy carries. V3 trains the GEMMs in 8-bit floating point — which, if you've read the quantisation page, you'll recognise as the same argument one octave further down. Two formats exist and neither is good enough alone:
format exp man max finite min normal steps per octave
e4m3 4 3 448 1.562e-02 8
e5m2 5 2 57,344 6.104e-05 4
e4m3 has the precision and not the range. e5m2 has the range and not the precision. Cast real tensors straight in and watch which dies:
direct cast, one tensor, no scaling -- relative L2 error:
tensor e4m3 e5m2 e4m3 finite?
weights (N(0,0.02)) 0.310877 0.051356 True
activation (N(0,1)) 0.027098 0.053512 True
act+outlier (x1500) inf 0.024086 False
grads (N(0,1e-5)) 1.000000 1.000000 True
which tensor dies first: the one with the outlier.
max |activation| = 1,500.0 vs e4m3 max 448
values that became inf: 1
gradients underflow e4m3's min normal: 4096 of 4096 -> 0
One value out of 4096 exceeded 448 and the tensor is gone. A single FP32 scale per tensor brings it back — but that scale is set by the loudest value, and the quiet ones pay for it. Smaller blocks, more scales, fewer dead values:
direct cast, no scale : inf <- overflowed, the tensor is gone
one FP32 scale : 0.001151 finite again
4096 activations, N(0,1), with four values of 3000 dropped in.
Error measured on the 4092 ORDINARY values only:
block err on quiet values dead (rounded to 0) bits/value
4096 0.031677 343 8.008
512 0.029651 184 8.062
128 0.026348 38 8.250
32 0.024253 12 9.000
That is V3's fine-grained quantisation: 1×128 tiles for activations, 128×128 for weights. The format did not get better. The blocks got smaller. The report pairs it with FP32 accumulation and wider master weights and optimiser state, because the places 8 bits are unacceptable are exactly the places error compounds. What it buys:
one activation tile, 4096 tokens x 7168:
BF16 58.72 MB
FP8 (1x128 scales) 30.28 MB
all-to-all bytes/token: BF16 dispatch 344,064 B ->
FP8 dispatch 258,048 B (25.0% less)
Multi-token prediction: two targets for one forward pass
A standard language model gets one training signal per position: predict the next token, as on the GPT page. V3 adds a second head that predicts the token after that, sequentially, keeping the full causal chain. Same forward pass, twice the targets.
micro-batch: 4096 positions
MTP depth 0 (1 head): 4,096 prediction targets per forward pass (1.0x)
MTP depth 1 (2 heads): 8,192 prediction targets per forward pass (2.0x)
MTP depth 2 (3 heads): 12,288 prediction targets per forward pass (3.0x)
what each head predicts, for the sentence positions 0..4:
pos 0 (the ) -> head 1: cat head 2: ate
pos 1 (cat ) -> head 1: ate head 2: the
pos 2 (ate ) -> head 1: the head 2: food
V3 uses depth 1 — one extra head. It is a training objective; the extra head is dropped for ordinary inference. But you can keep it, and then it is a draft model that costs almost nothing, which is speculative decoding with the drafter already inside the target model. The report puts second-token acceptance at 85–90% and reports roughly 1.8× decoding throughput from using it that way.
expected tokens accepted per decoding step = 1 + p(second accepted):
acceptance 0.85 -> 1.85 tokens/step (1.85x, before the extra head's own cost)
acceptance 0.90 -> 1.90 tokens/step (1.90x, before the extra head's own cost)
The 1 + p arithmetic is mine, not theirs, and it ignores verification cost. The 85–90% and the 1.8× are the report's.
Overlapping communication with compute
You cannot delete the all-to-all. You can make it happen while the GPU is doing something else — the async call you already write without thinking. My script simulates a 1F1B pipeline, 8 stages, 32 microbatches, and asks what the device is doing. This is a simulation of a schedule, not a measurement of DeepSeek's.
pipeline: 8 stages, 32 microbatches, compute 1.0, all-to-all 0.8 per microbatch
schedule makespan device busy bubble
no overlap 70.20 45.6% 54.4%
comm hidden 39.00 82.1% 17.9%
wall-clock penalty for not overlapping: 1.80x
illustration only: a 56.7-day run with this shape becomes 102.1 days
The second column is the one worth staring at. Once communication is hidden, making it more expensive stops mattering until it exceeds compute:
comm bubble (no overlap) bubble (overlapped) speedup
0.2 31.6% 17.9% 1.20x
0.5 45.3% 17.9% 1.50x
0.8 54.4% 17.9% 1.80x
1.2 62.7% 31.6% 1.83x
2.0 72.6% 59.0% 1.50x
What "went below the abstraction layer" actually means
Three things in the report that most write-ups skip, and that are the reason the three fixes above were achievable rather than aspirational:
| What they did | Why |
|---|---|
| Wrote PTX, NVIDIA's assembly-level layer below CUDA, for the communication kernels | The dispatch/combine kernels had to be tuned by hand. Inline asm, because the compiler wouldn't do it |
| Reserved a fraction of each GPU's streaming multiprocessors for communication | Comm and compute genuinely run at once. You budget the cores, the way you'd pin a thread pool |
| Avoided tensor parallelism entirely | It is the most communication-hungry parallelism there is — it talks inside every layer. On a narrow link it's the first thing to go |
Multi-head latent attention is theirs too, from DeepSeek-V2: it compresses the key-value cache into one low-rank latent vector per token, so long-context serving stops being a memory problem. It has its own page — multi-head latent attention, built on the byte counting from the KV cache page — so I've deliberately not re-derived it here.
The number everyone gets wrong
You will be asked about the $5.6M. Do the division yourself once and you'll never repeat it carelessly:
final pre-training GPU-hours (report) : 2,788,000 H800-hours
assumed rental price (report's own) : $2.00 per GPU-hour
product : $5.576M
on 2048 GPUs that is 56.7 days of wall clock
tokens: 14.8e12 over 671e9 params, 37e9 active per token
tokens per active parameter : 400
approx forward+backward FLOPs (6*N*D) : 3285.6 ZFLOP
implied MFU against BF16 peak : 33.1%
against the FP8 peak they actually used: 16.5%
The 400 tokens per active parameter is worth a second look next to the scaling laws page — it is far past Chinchilla-optimal, which is what you do when you intend to serve the model rather than win a compute-optimal argument.
எல்லோரும் படித்த மாதிரிதான் நானும் இந்த report-ஐப் படித்தேன் — ஏதோ ஒரு தந்திரத்தைத் தேடிக்கொண்டு. அப்படி ஒன்று இல்லை. ஏறத்தாழ எட்டு இருக்கின்றன, அவை எல்லாம் ஒரே திசையைக் காட்டுகின்றன, அவர்கள் எந்த GPU-க்களில் இதைச் செய்தார்கள் என்பதைக் கவனிக்கும் வரை அந்தத் திசை எனக்குத் தெரியவில்லை.
US export controls காரணமாக DeepSeek H800-களில் train செய்தது. கணக்கீட்டைப் பொறுத்தவரை H100 அதே chip தான். ஆனால் NVLink கிட்டத்தட்ட பாதி — server-க்குள் chip-க்கும் chip-க்கும் இடையேயான link. ஒரு mixture-of-experts model-க்கு இதுதான் மிகக் கொடூரமான வெட்டு, ஏனெனில் tokens-ஐ experts-க்கு route செய்வது ஒரு all-to-all, குறுகிய link ஒரு all-to-all-ஐத்தான் மிகக் கடுமையாகத் தண்டிக்கும்.
இந்த system-ஐ நீங்கள் ஏற்கனவே கட்டியிருக்கிறீர்கள்
ஒரு request-ஐப் பல backends-க்கு fan-out செய்து பதில்களை merge செய்யும் service உங்களிடம் இருக்கிறது. வேகமான local network-இல் நீங்கள் இதை வெளிப்படையான வழியில் எழுதுவீர்கள்:
def handle(req):
picks = router.pick(req, k=8) # 8 backends, எங்கிருந்தாலும்
return merge([call(b, req) for b in picks]) # ஒவ்வொன்றுக்கும் payload
பிறகு அதை மூன்றில் ஒரு பங்கு வேகமுள்ள link-க்கு நகர்த்துகிறீர்கள், அது விழுந்துவிடுகிறது. backends மெதுவாகிவிட்டதால் அல்ல — அவை மாறவே இல்லை — ஆனால் payload இப்போது handler-இல் இருப்பதைவிட wire-இல் அதிக நேரம் செலவழிக்கிறது. மூன்று திருத்தங்களும் உங்களுக்குத் தெரியும், ஏனெனில் மூன்றையும் நீங்கள் ship செய்திருக்கிறீர்கள்:
def handle(req):
picks = router.pick(req, k=8, max_racks=4) # 1. fan-out-ஐ cap செய்
payload = compress(req) # 2. ஒவ்வொரு byte-ஐயும் சுருக்கு
fut = send_async(picks, payload) # 3. wire-ஐ வேலையுடன் overlap செய்
local = do_other_work()
return merge(fut.result(), local)
DeepSeek-V3-இன் engineering பாதி இதுதான், மூன்று வரிகளில். fan-out-ஐ cap செய்வது node-limited routing. ஒவ்வொரு byte-ஐயும் சுருக்குவது FP8. wire-ஐ வேலையுடன் overlap செய்வது DualPipe. மீதி எல்லாம், இந்த மூன்றையும் உண்மையில் நடக்கவைக்க abstraction layer-க்குக் கீழே அவர்கள் எவ்வளவு தூரம் இறங்கத் தயாராக இருந்தார்கள் என்பதன் விவரம்.
| சொல் | அர்த்தம் | Ops உவமை |
|---|---|---|
| All-to-all | ஒவ்வொரு GPU-வும் தேர்ந்தெடுக்கப்பட்ட expert வைத்திருக்கும் ஒவ்வொரு GPU-க்கும் தன் tokens-இன் ஒரு பகுதியை அனுப்புகிறது, பிறகு முடிவுகளைத் திரும்பப் பெறுகிறது | ஒரு shuffle stage. உங்கள் MapReduce-இல் உண்மையில் காசு பிடுங்கும் பகுதி |
| NVLink | ஒரே server-க்கு உள்ளே GPU-க்களுக்கு இடையேயான link. H100-இல் 900 GB/s, H800-இல் 400 GB/s | Rack-க்குள்ளான network |
| InfiniBand | server-களுக்கு இடையேயான link. 400 Gbps = ஒரு GPU-க்கு 50 GB/s | Datacentre-க்கு இடையே. அதே request, பத்து மடங்கு latency budget |
| Expert parallelism | Experts வெவ்வேறு GPU-க்களில் இருக்கின்றன, எனவே tokens அவற்றை அடையப் பயணிக்க வேண்டும் | Key மூலம் sharding. shuffle தான் விலை |
| Bubble | அடுத்த stage-க்காகக் காத்திருந்து சும்மா உட்கார்ந்திருக்கும் ஒரு pipeline stage | பின்னால் ஒன்றும் queue இல்லாமல் IO-வில் block ஆன ஒரு worker |
| PTX | NVIDIA-வின் assembly அளவிலான layer, CUDA C-க்கு ஒரு படி கீழே | compiler நீங்கள் விரும்புவதைச் செய்யாததால் inline asm-க்கு இறங்குவது |
அந்த ஒரு சூத்திரம்
Loss function அல்ல. இங்கே முக்கியமான சூத்திரம், உங்கள் GPU-க்கள் கணக்கிடுகின்றனவா அல்லது காத்திருக்கின்றனவா என்பதைத் தீர்மானிப்பது:
இதை pipeline stages-ஆகப் படியுங்கள்:
| படி | ஒரு MoE layer-க்கு | என்ன செய்கிறது |
|---|---|---|
FLOPs | ஒரு token-க்கு k × 3 × 2 × dmodel × dffn | Top-8 SwiGLU experts. தலா மூன்று matrices, ஒரு multiply-add-க்கு இரண்டு flops |
bytes | IB-இல் nodes × dmodel × (1 + 2), NVLink-இல் k × dmodel × (1 + 2) | ஒவ்வொரு destination node-க்கும் ஒரு நகல் வெளியே, முடிவுகள் திரும்ப. வெளியே FP8, திரும்ப BF16 |
throughput | 1979 TFLOPS FP8 | H100-லும் H800-லும் ஒரே மாதிரி. export controls தொடாத எண் இதுதான் |
bandwidth | 900 அல்லது 400 GB/s | இதுதான் முழுப் paper. அவர்கள் தொட்ட எண் இதுதான் |
விகிதம் < 1 | compute-bound | உங்கள் tensor cores நேரத்தை நிர்ணயிக்கின்றன. Model-ஐப் பெரிதாக்குங்கள் |
விகிதம் > 1 | comm-bound | உங்கள் wire நேரத்தை நிர்ணயிக்கிறது. ஒவ்வொரு design முடிவும் மாறுகிறது |
கட்டுப்பாடு, எண்ணிப் பார்த்தால்
DeepSeek-V3 மொத்தம் 671B parameters, ஒரு token-க்கு 37B active, 61 layers, dmodel 7168, 256 routed experts மற்றும் ஒரு shared, top-8 routing, expert hidden size 2048. 4096 tokens micro-batch கொண்ட ஒரு GPU-வை ஒரு MoE layer வழியாக எடுத்துக்கொண்டு எண்ணுங்கள்.
கீழே உள்ள bandwidth எண்கள் வெளியிடப்பட்ட specifications, நான் அளந்தவை அல்ல, "நேரங்கள்" என்பவை அந்த specifications-ஆல் bytes-ஐ வகுத்த முடிவுகள். HBM traffic-க்காக FlashAttention page செய்யும் அதே வகையான எண்ணிக்கைதான் இது — ஒரு roofline வரைபடம், profile அல்ல.
என் வார்த்தையை நம்ப வேண்டாம். உண்மையான output இதோ:
tokens on this GPU : 4096
d_model : 7168, top-8 routed experts, capped at 4 nodes
expert FLOPs per token : 704.6 MFLOP
expert FLOPs, this batch : 2.89 TFLOP
InfiniBand bytes (out+in): 352.3 MB (4 node copies x (1B dispatch + 2B combine))
NVLink bytes (out+in): 704.6 MB (8 expert copies inside the nodes)
arithmetic intensity of this layer:
2,731 FLOP per byte moved
machine balance (FLOP per byte the link can feed, FP8 compute):
H100 NVLink 900 GB/s: 2,199
H800 NVLink 400 GB/s: 4,948
InfiniBand 50 GB/s: 39,580
2,731 FLOP per byte என்பது இந்த layer தருவது. 2,199 என்பது ஒரு H100-இன் NVLink ஊட்ட முடிவது. 4,948 என்பது H800-இன் அளவு. Layer இரண்டுக்கும் நடுவே அமர்ந்திருக்கிறது. முழுக் கதையும் இதுதான், இதோ நேரமாக:
machine compute NVLink leg comm/compute verdict
H100 900 GB/s 1.46ms 0.78ms 0.54x COMPUTE-bound
H800 400 GB/s 1.46ms 1.76ms 1.21x COMM-bound
same layer, same arithmetic, one number on a spec sheet halved,
and the bottleneck moves from the tensor cores to the wire.
மேலும் cross-node leg — export controls தொடாத பகுதி — இரண்டையும் விட மோசம். node cap ஏன் இருக்கிறது என்பதற்குக் காரணமே இதுதான்:
the cross-node leg, which the export cut did NOT touch:
H100: IB 7.05ms + NVLink 0.78ms = 5.37x compute (9.0x the NVLink leg)
H800: IB 7.05ms + NVLink 1.76ms = 6.04x compute (4.0x the NVLink leg)
Export cut NVLink-ai dhaan pathiya aakkichu, InfiniBand-ai illa. Aana cross-node leg dhaan rendu case-la-yum periya cost — adhanaala dhaan node cap.
InfiniBand மற்றும் NVLink legs-ஐ overlap செய்ய முடியும் வகையில் node cap-ஐத் தாங்கள் tune செய்ததாக report வெளிப்படையாகச் சொல்கிறது, செயல்முறையில் கிடைக்கும் NVLink-க்கும் IB-க்கும் இடையிலான bandwidth விகிதம் சுமார் 3.2:1 என்று மேற்கோள் காட்டுகிறது. அதன் பொருள், அடையப்படும் NVLink எண் 400-ஐ விட 160 GB/s-க்கு அருகில் இருக்கும். என் 900 மற்றும் 400-ஐ யாரும் எட்டாத உச்சவரம்புகளாகவே எடுத்துக்கொள்ளுங்கள்; table-இன் வடிவம்தான் முக்கியம், தசம இலக்கங்கள் அல்ல.
Auxiliary-loss-free balancing: paper-இன் மிக நேர்த்தியான யோசனை
சிலரையே பிரியப்படும் ஒரு router என்பது hot-shard பிரச்சனை, அதை MoE page வழக்கமான முறையில் சரிசெய்கிறது: சமமற்ற routing-ஐத் தண்டிக்கும் ஒரு auxiliary loss சேர்ப்பது. அது வேலை செய்கிறது. ஆனால் அது ஒரு gradient-ஐயும் சேர்க்கிறது — கட்டமைப்பாலேயே அது task-இன் gradient அல்ல. Router-ஐ, சரியாக இருப்பதற்கு பதிலாக சமநிலையாக இருக்கச் சொல்கிறீர்கள்.
V3 அதை ஒரு SRE உடனே அடையாளம் காணும் ஒன்றால் மாற்றுகிறது: ஒரு thermostat.
selection_score = affinity + bias # bias யாரைத் தேர்வு செய்வது என்பதை மாற்றும்
gate = affinity # loss-ஐ அல்ல, எனவே grad(bias) = 0
bias[expert] -= gamma * sign(load[expert] - mean_load) # ஒவ்வொரு step-க்குப் பின்
சுமை அதிகமான expert, bias கீழே. பட்டினி கிடக்கும் expert, bias மேலே. நிலையான step, sign மட்டும். இது selection-ஐ நகர்த்துகிறது, gradient-ஐத் தொடுவதில்லை. என் script ஒரு பொம்மை உலகை அமைக்கிறது, அதில் token demand Zipf-skewed — அதாவது model விரும்பும் routing உண்மையிலேயே சமமற்றது, எனவே balance-ம் தரமும் நிஜமாகவே சண்டையிடுகின்றன.
true expert demand (Zipf, the routing the model actually wants):
[162 90 73 49 43 35 27 33]
max/min expert occupancy, by step (1.0 = perfectly balanced):
step none aux-loss bias (V3)
0 11.2 11.2 11.2
10 8.7 3.8 8.1
20 6.6 3.2 2.0
40 6.1 3.0 2.2
59 6.1 3.0 2.1
final per-expert token counts (512 tokens, top-1):
none : [165 89 74 51 41 33 27 32]
aux-loss: [121 92 71 56 48 41 42 41]
bias : [80 53 60 74 62 50 43 90]
V3 bias vector: [-3. -1.8 1.2 1.2 3. 3.6 3.6 3. ]
இரண்டுமே balance செய்கின்றன. இப்போது இந்த யோசனையையே தூண்டிய எண் — balancing முறை router-இன் weights-க்குச் சேர்க்கும் gradient, மற்றும் bias-ஐத் திரும்ப எடுத்தபின் router இன்னும் எவ்வளவு சரியாக expert-ஐத் தேர்ந்தெடுக்கிறது:
gradient norm contributed by the balancing term:
aux-loss : 0.434286 <- fights the routing the model wanted
bias (V3): 0.000000 <- exactly zero, by construction
router accuracy vs the true best expert (bias removed):
no balancing : 0.9590
aux-loss : 0.8848
bias (V3) : 0.9590
Aux loss balance kudukkudhu, aana router accuracy-ai vaangikkidhu. V3-oda bias loss-la illave illa — gradient exactly zero, accuracy adhe 0.9590.
சிறந்த balance மற்றும் accuracy இழப்பு இல்லை. auxiliary weight-ஐ ஏற்றினால், paper தவிர்க்கக் கட்டப்பட்ட தோல்வியே கிடைக்கும் — ஏற்கனவே உங்களிடம் இருந்த balance-ஐத் தரத்தைக் கொடுத்து வாங்குகிறீர்கள்:
BREAK IT: crank the auxiliary loss weight to alpha=6.0
balance max/min : 3.1 (aux 1.50: 3.0, bias: 2.1)
router accuracy : 0.7324 <- balanced, and worse
balancing grad : 1.323678
Thermostat-க்கும் அதன் சொந்தத் தோல்வி முறை உண்டு, நீங்கள் ஊகிப்பது அதுவே:
BREAK IT: bias update rate gamma=8.0 instead of 0.6
max/min occupancy, last 8 steps:
[202. 81.8 224. 202. 63.6 153. 63.4 153. ]
vs gamma=0.6: [2. 1.6 2. 1.6 2. 1.6 1.3 2.1]
final bias vector: [144. 144. 144. 144. 144. 152. 144. 144.]
Node-limited routing: fan-out-ஐ cap செய்வது
ஒரு token தன் top-8 experts-ஐ, அவை எங்கிருந்தாலும் விரும்புகிறது. சும்மா விட்டால் அது எட்டு வெவ்வேறு servers-ஐத் தொடலாம், ஒவ்வொரு கூடுதல் server-ம் அதே 7168-அகல vector-இன் இன்னொரு InfiniBand நகல், அதுவும் இயந்திரத்தின் மிக மெதுவான link-இல். Cap இதுதான்: ஒவ்வொரு node-ஐயும் அதன் experts-இன் affinities கூட்டுத்தொகையால் மதிப்பிடு, சிறந்த M nodes-ஐ வைத்துக்கொள், top-8-ஐ அவற்றிலிருந்து மட்டும் தேர்ந்தெடு. V3 M = 4 பயன்படுத்துகிறது.
256 experts over 32 nodes (8 experts per node), top-8, 4096 tokens
uncapped: mean 6.652 nodes/token, max 8
capped : mean 3.748 nodes/token, max 4 (M=4)
InfiniBand bytes, uncapped: 585.9 MB (11.72 ms at 50 GB/s)
InfiniBand bytes, capped : 330.1 MB (6.60 ms at 50 GB/s)
saved: 43.7%
what the cap costs -- experts still reachable per token:
uncapped : 256 of 256 (100.0%)
capped : 32 of 256 (12.5%)
mean overlap with the uncapped choice: 3.62 of 8 experts
இது இலவசம் அல்ல, எவ்வளவு இலவசம் இல்லை என்பதை script துல்லியமாக அச்சிடுகிறது: cap செய்யப்பட்ட ஒரு token, தான் தேர்ந்தெடுத்திருக்கும் 8 experts-இல் 3.62-ஐ மட்டுமே பெறுகிறது, மேலும் expert pool-இல் எட்டில் ஒரு பங்கு மட்டுமே எட்டக்கூடியது. Model-இன் சுதந்திரத்தை wire நேரத்திற்காக விற்கிறீர்கள். Cap-ஐ sweep செய்து பேரத்தைப் பாருங்கள்:
M=2: 2.000 nodes/token, 176.1 MB IB, overlap 2.57/8
M=4: 3.748 nodes/token, 330.1 MB IB, overlap 3.62/8
M=8: 5.330 nodes/token, 469.5 MB IB, overlap 5.00/8
M=32: 6.652 nodes/token, 585.9 MB IB, overlap 8.00/8
Traffic-ஐக் குறைக்க M தேர்ந்தெடுக்கப்படவில்லை — அதைச் செய்ய M=2 போதும். அந்த 3.2:1 விகிதத்தைக் கருதி, IB leg-ம் NVLink leg-ம் ஒன்றின் கீழ் ஒன்று மறையும் அளவுக்கு நெருக்கமாக இருக்கும்படி அது தேர்ந்தெடுக்கப்பட்டது. Cap-ம் overlap-ம் ஒரே design, இரண்டு அல்ல.
FP8: byte குறைப்பின் மறு பாதி
fan-out-ஐப் பாதியாக்கினால் நகல்கள் பாதியாகின்றன. payload-ஐப் பாதியாக்கினால் ஒவ்வொரு நகலும் சுமப்பது பாதியாகிறது. V3 GEMM-களை 8-bit floating point-இல் train செய்கிறது — quantisation page-ஐப் படித்திருந்தால், அதே வாதம் ஒரு octave கீழே என்பதை அடையாளம் காண்பீர்கள். இரண்டு formats இருக்கின்றன, தனியாக எதுவும் போதுமானதல்ல:
format exp man max finite min normal steps per octave
e4m3 4 3 448 1.562e-02 8
e5m2 5 2 57,344 6.104e-05 4
e4m3-க்கு precision உண்டு, range இல்லை. e5m2-க்கு range உண்டு, precision இல்லை. நிஜமான tensors-ஐ நேராக cast செய்து எது சாகிறது என்று பாருங்கள்:
direct cast, one tensor, no scaling -- relative L2 error:
tensor e4m3 e5m2 e4m3 finite?
weights (N(0,0.02)) 0.310877 0.051356 True
activation (N(0,1)) 0.027098 0.053512 True
act+outlier (x1500) inf 0.024086 False
grads (N(0,1e-5)) 1.000000 1.000000 True
which tensor dies first: the one with the outlier.
max |activation| = 1,500.0 vs e4m3 max 448
values that became inf: 1
gradients underflow e4m3's min normal: 4096 of 4096 -> 0
4096-இல் ஒரே ஒரு மதிப்பு 448-ஐத் தாண்டியது, tensor போய்விட்டது. ஒரு tensor-க்கு ஒரு FP32 scale அதைத் திரும்பக் கொண்டுவருகிறது — ஆனால் அந்த scale-ஐ மிகப் பெரிய மதிப்புதான் நிர்ணயிக்கிறது, அமைதியானவை அதற்கு விலை கொடுக்கின்றன. சிறிய blocks, அதிக scales, குறைவான இறந்த மதிப்புகள்:
direct cast, no scale : inf <- overflowed, the tensor is gone
one FP32 scale : 0.001151 finite again
4096 activations, N(0,1), with four values of 3000 dropped in.
Error measured on the 4092 ORDINARY values only:
block err on quiet values dead (rounded to 0) bits/value
4096 0.031677 343 8.008
512 0.029651 184 8.062
128 0.026348 38 8.250
32 0.024253 12 9.000
V3-இன் fine-grained quantisation இதுதான்: activations-க்கு 1×128 tiles, weights-க்கு 128×128. Format மேம்படவில்லை. Blocks சிறியதாகின. Report இதை FP32 accumulation, அகலமான master weights மற்றும் optimiser state-உடன் இணைக்கிறது, ஏனெனில் 8 bits ஏற்கத்தகாத இடங்கள்தான் பிழை குவியும் இடங்கள். இது வாங்கித் தருவது:
one activation tile, 4096 tokens x 7168:
BF16 58.72 MB
FP8 (1x128 scales) 30.28 MB
all-to-all bytes/token: BF16 dispatch 344,064 B ->
FP8 dispatch 258,048 B (25.0% less)
Multi-token prediction: ஒரு forward pass, இரண்டு targets
ஒரு வழக்கமான language model-க்கு ஒவ்வொரு position-லும் ஒரே ஒரு training signal: அடுத்த token-ஐக் கணி, GPT page-இல் உள்ளதுபோல. V3 ஒரு இரண்டாவது head-ஐச் சேர்க்கிறது, அது அதற்கு அடுத்த token-ஐ வரிசையாகக் கணிக்கிறது, முழு causal சங்கிலியையும் காப்பாற்றுகிறது. அதே forward pass, இரண்டு மடங்கு targets.
micro-batch: 4096 positions
MTP depth 0 (1 head): 4,096 prediction targets per forward pass (1.0x)
MTP depth 1 (2 heads): 8,192 prediction targets per forward pass (2.0x)
MTP depth 2 (3 heads): 12,288 prediction targets per forward pass (3.0x)
what each head predicts, for the sentence positions 0..4:
pos 0 (the ) -> head 1: cat head 2: ate
pos 1 (cat ) -> head 1: ate head 2: the
pos 2 (ate ) -> head 1: the head 2: food
V3 depth 1 பயன்படுத்துகிறது — ஒரு கூடுதல் head. இது ஒரு training நோக்கம்; வழக்கமான inference-க்கு அந்தக் கூடுதல் head கைவிடப்படுகிறது. ஆனால் அதை வைத்துக்கொள்ளலாம், அப்போது அது ஏறக்குறைய செலவே இல்லாத ஒரு draft model — அதாவது target model-க்குள்ளேயே drafter இருக்கும் speculative decoding. இரண்டாவது token-இன் acceptance-ஐ 85–90% என்று report சொல்கிறது, அப்படிப் பயன்படுத்தினால் decoding throughput சுமார் 1.8× என்று தெரிவிக்கிறது.
expected tokens accepted per decoding step = 1 + p(second accepted):
acceptance 0.85 -> 1.85 tokens/step (1.85x, before the extra head's own cost)
acceptance 0.90 -> 1.90 tokens/step (1.90x, before the extra head's own cost)
1 + p கணக்கு என்னுடையது, அவர்களுடையது அல்ல, verification செலவை அது கணக்கில் எடுக்கவில்லை. 85–90%-ம் 1.8×-ம் report-இன் எண்கள்.
Communication-ஐ compute-உடன் overlap செய்வது
all-to-all-ஐ நீங்கள் அழிக்க முடியாது. GPU வேறு ஏதோ செய்துகொண்டிருக்கும்போது அதை நடக்கவைக்க முடியும் — யோசிக்காமலேயே நீங்கள் எழுதும் அந்த async call. என் script ஒரு 1F1B pipeline-ஐ simulate செய்கிறது, 8 stages, 32 microbatches, device என்ன செய்கிறது என்று கேட்கிறது. இது ஒரு schedule-இன் simulation, DeepSeek-இன் அளவீடு அல்ல.
pipeline: 8 stages, 32 microbatches, compute 1.0, all-to-all 0.8 per microbatch
schedule makespan device busy bubble
no overlap 70.20 45.6% 54.4%
comm hidden 39.00 82.1% 17.9%
wall-clock penalty for not overlapping: 1.80x
illustration only: a 56.7-day run with this shape becomes 102.1 days
இரண்டாவது நெடுவரிசையைத்தான் உற்றுப் பார்க்க வேண்டும். communication மறைக்கப்பட்டவுடன், அதை அதிக விலையுள்ளதாக ஆக்குவது, அது compute-ஐத் தாண்டும் வரை முக்கியமில்லாமல் போகிறது:
comm bubble (no overlap) bubble (overlapped) speedup
0.2 31.6% 17.9% 1.20x
0.5 45.3% 17.9% 1.50x
0.8 54.4% 17.9% 1.80x
1.2 62.7% 31.6% 1.83x
2.0 72.6% 59.0% 1.50x
"abstraction layer-க்குக் கீழே போனது" என்பதன் உண்மையான அர்த்தம்
பெரும்பாலான விளக்கங்கள் தவிர்க்கும் மூன்று விஷயங்கள் report-இல் இருக்கின்றன, மேலே சொன்ன மூன்று திருத்தங்களும் கனவாக இல்லாமல் நிஜமாக முடிந்ததற்கு அவைதான் காரணம்:
| அவர்கள் செய்தது | ஏன் |
|---|---|
| communication kernels-க்காக CUDA-க்குக் கீழே உள்ள NVIDIA-வின் assembly அளவிலான layer PTX எழுதினார்கள் | dispatch/combine kernels கையால் tune செய்யப்பட வேண்டியிருந்தது. Inline asm, ஏனெனில் compiler அதைச் செய்யாது |
| ஒவ்வொரு GPU-வின் streaming multiprocessors-இல் ஒரு பகுதியை communication-க்காக ஒதுக்கினார்கள் | Comm-ம் compute-ம் உண்மையிலேயே ஒரே நேரத்தில் ஓடுகின்றன. ஒரு thread pool-ஐ pin செய்வதுபோல cores-ஐ budget செய்கிறீர்கள் |
| Tensor parallelism-ஐ முற்றிலும் தவிர்த்தார்கள் | அதுதான் மிக அதிக communication தேவைப்படும் parallelism — ஒவ்வொரு layer-க்குள்ளும் பேசுகிறது. குறுகிய link-இல் முதலில் நீக்க வேண்டியது அதுதான் |
Multi-head latent attention-ம் அவர்களுடையதுதான், DeepSeek-V2-இலிருந்து: அது key-value cache-ஐ ஒரு token-க்கு ஒரு low-rank latent vector-ஆகச் சுருக்குகிறது, எனவே long-context serving ஒரு memory பிரச்சனையாக இருப்பது நின்றுவிடுகிறது. அதற்குத் தனிப் பக்கம் இருக்கிறது — multi-head latent attention, KV cache page-இன் byte எண்ணிக்கையின் மேல் கட்டப்பட்டது — எனவே இங்கே வேண்டுமென்றே அதை மீண்டும் விளக்கவில்லை.
எல்லோரும் தவறாகச் சொல்லும் எண்
$5.6M பற்றி உங்களிடம் கேட்கப்படும். ஒரு முறை நீங்களே வகுத்துப் பாருங்கள், பிறகு அதை அஜாக்கிரதையாக ஒருபோதும் திரும்பச் சொல்ல மாட்டீர்கள்:
final pre-training GPU-hours (report) : 2,788,000 H800-hours
assumed rental price (report's own) : $2.00 per GPU-hour
product : $5.576M
on 2048 GPUs that is 56.7 days of wall clock
tokens: 14.8e12 over 671e9 params, 37e9 active per token
tokens per active parameter : 400
approx forward+backward FLOPs (6*N*D) : 3285.6 ZFLOP
implied MFU against BF16 peak : 33.1%
against the FP8 peak they actually used: 16.5%
active parameter-க்கு 400 tokens என்பதை scaling laws page-உடன் சேர்த்து ஒரு முறை பார்ப்பது நல்லது — அது Chinchilla-optimal-ஐ வெகுவாகத் தாண்டியது, compute-optimal வாதத்தை வெல்வதற்குப் பதிலாக model-ஐ serve செய்யப் போகிறோம் என்று முடிவெடுக்கும்போது நீங்கள் செய்வது அதுதான்.
मैंने यह report वैसे ही पढ़ी जैसे सबने पढ़ी — कोई तरकीब ढूँढते हुए। ऐसी कोई एक तरकीब है ही नहीं। करीब आठ हैं, और वे सब एक ही दिशा में इशारा करती हैं, और वह दिशा मुझे तब तक नहीं दिखी जब तक मैंने यह नहीं देखा कि वे किन GPU-पर काम कर रहे थे।
US export controls की वजह से DeepSeek ने H800 पर train किया। गणित के लिहाज़ से वही chip जो H100 है। लेकिन लगभग आधा NVLink — यानी server के भीतर chip-से-chip वाला link। एक mixture-of-experts model के लिए यह सबसे क्रूर कटौती है, क्योंकि tokens को experts तक route करना एक all-to-all है, और all-to-all ही वह चीज़ है जिसे सँकरा link सबसे ज़्यादा सज़ा देता है।
यह system आप पहले बना चुके हैं
आपके पास एक service है जो एक request को कई backends में fan out करती है और जवाब merge करती है। तेज़ local network पर आप इसे सीधे-सीधे लिखते हैं:
def handle(req):
picks = router.pick(req, k=8) # 8 backends, कहीं भी हों
return merge([call(b, req) for b in picks]) # हर एक को payload भेजो
फिर आप इसे एक तिहाई रफ़्तार वाले link पर ले जाते हैं, और यह गिर जाती है। इसलिए नहीं कि backends धीमे हो गए — वे वही हैं — बल्कि इसलिए कि payload अब handler के मुक़ाबले wire पर ज़्यादा समय बिताता है। तीनों उपाय आप जानते ही हैं, क्योंकि तीनों आप ship कर चुके हैं:
def handle(req):
picks = router.pick(req, k=8, max_racks=4) # 1. fan-out को cap करो
payload = compress(req) # 2. हर byte को छोटा करो
fut = send_async(picks, payload) # 3. wire को काम से overlap करो
local = do_other_work()
return merge(fut.result(), local)
यही DeepSeek-V3 का engineering वाला आधा हिस्सा है, तीन लाइनों में। fan-out को cap करना node-limited routing है। हर byte को छोटा करना FP8 है। wire को काम से overlap करना DualPipe है। बाक़ी सब इस बात का ब्यौरा है कि इन तीनों को सचमुच करने के लिए वे abstraction layer के कितने नीचे तक जाने को तैयार थे।
| शब्द | मतलब | Ops की उपमा |
|---|---|---|
| All-to-all | हर GPU अपने tokens का एक हिस्सा उस हर GPU को भेजता है जिस पर चुना गया expert है, फिर नतीजे वापस लेता है | एक shuffle stage. आपके MapReduce का वह हिस्सा जो असल में पैसा खाता है |
| NVLink | एक ही server के भीतर GPU-के बीच का link. H100 पर 900 GB/s, H800 पर 400 GB/s | Rack के भीतर का network |
| InfiniBand | servers के बीच का link. 400 Gbps = प्रति GPU 50 GB/s | Datacentre के आर-पार. वही request, दस गुना latency budget |
| Expert parallelism | Experts अलग-अलग GPU पर रहते हैं, इसलिए tokens को उन तक जाना पड़ता है | Key से sharding. shuffle ही क़ीमत है |
| Bubble | एक pipeline stage जो अपने पड़ोसी का इंतज़ार करते हुए ख़ाली बैठा है | IO पर block हुआ worker, जिसके पीछे कोई काम queue में नहीं |
| PTX | NVIDIA की assembly-स्तर की layer, CUDA C से एक नीचे | Inline asm पर उतरना, क्योंकि compiler वह नहीं करेगा जो आप चाहते हैं |
वह एक फ़ॉर्मूला
Loss function नहीं। यहाँ जो फ़ॉर्मूला मायने रखता है वह यह तय करता है कि आपके GPU गणना कर रहे हैं या इंतज़ार:
इसे pipeline stages की तरह पढ़िए:
| चरण | एक MoE layer के लिए | यह क्या करता है |
|---|---|---|
FLOPs | प्रति token k × 3 × 2 × dmodel × dffn | Top-8 SwiGLU experts. हर एक में तीन matrices, प्रति multiply-add दो flops |
bytes | IB पर nodes × dmodel × (1 + 2), NVLink पर k × dmodel × (1 + 2) | हर destination node के लिए एक copy बाहर, नतीजे वापस. बाहर FP8, वापस BF16 |
throughput | 1979 TFLOPS FP8 | H100 और H800 पर एक जैसा. यही वह number है जिसे export controls ने छुआ नहीं |
bandwidth | 900 या 400 GB/s | यही पूरा paper है. यही वह number है जिसे उन्होंने छुआ |
अनुपात < 1 | compute-bound | घड़ी आपके tensor cores चलाते हैं. Model को बड़ा कीजिए |
अनुपात > 1 | comm-bound | घड़ी आपका wire चलाता है. हर design फ़ैसला बदल जाता है |
वह अड़चन, गिनकर
DeepSeek-V3 में कुल 671B parameters हैं, प्रति token 37B active, 61 layers, dmodel 7168, 256 routed experts और एक shared, top-8 routing, expert hidden size 2048. 4096 tokens के micro-batch वाले एक GPU को एक MoE layer से गुज़ारिए और गिनिए।
नीचे दिए bandwidth के आँकड़े प्रकाशित specifications हैं, मेरे नापे हुए नहीं, और "समय" उन्हीं specifications से bytes को भाग देकर निकला है। यह वैसी ही गिनती है जैसी FlashAttention page HBM traffic के लिए करती है — एक roofline खाका, profile नहीं।
मेरी बात पर मत जाइए। असली output यह रहा:
tokens on this GPU : 4096
d_model : 7168, top-8 routed experts, capped at 4 nodes
expert FLOPs per token : 704.6 MFLOP
expert FLOPs, this batch : 2.89 TFLOP
InfiniBand bytes (out+in): 352.3 MB (4 node copies x (1B dispatch + 2B combine))
NVLink bytes (out+in): 704.6 MB (8 expert copies inside the nodes)
arithmetic intensity of this layer:
2,731 FLOP per byte moved
machine balance (FLOP per byte the link can feed, FP8 compute):
H100 NVLink 900 GB/s: 2,199
H800 NVLink 400 GB/s: 4,948
InfiniBand 50 GB/s: 39,580
2,731 FLOP per byte वह है जो यह layer देती है। 2,199 वह है जो एक H100 का NVLink खिला सकता है। 4,948 वह है जो H800 का। Layer इन दोनों के बीच बैठी है। पूरी कहानी यही है, और समय के रूप में यह रही:
machine compute NVLink leg comm/compute verdict
H100 900 GB/s 1.46ms 0.78ms 0.54x COMPUTE-bound
H800 400 GB/s 1.46ms 1.76ms 1.21x COMM-bound
same layer, same arithmetic, one number on a spec sheet halved,
and the bottleneck moves from the tensor cores to the wire.
और cross-node leg, जिसे export controls ने छुआ नहीं, दोनों से बुरी है — यही वजह है कि node cap है ही:
the cross-node leg, which the export cut did NOT touch:
H100: IB 7.05ms + NVLink 0.78ms = 5.37x compute (9.0x the NVLink leg)
H800: IB 7.05ms + NVLink 1.76ms = 6.04x compute (4.0x the NVLink leg)
Report साफ़ कहती है कि उसने node cap को इस तरह tune किया कि InfiniBand और NVLink दोनों legs को overlap किया जा सके, और वह NVLink-से-IB का प्रभावी bandwidth अनुपात क़रीब 3.2:1 बताती है। इसका मतलब है कि असल में मिलने वाला NVLink आँकड़ा 400 से ज़्यादा 160 GB/s के पास है। मेरे 900 और 400 को ऐसी छतें मानिए जिन तक कोई नहीं पहुँचता; बात table की शक्ल की है, दशमलव की नहीं।
Auxiliary-loss-free balancing: paper का सबसे सुंदर विचार
पक्षपात करने वाला router यानी hot-shard समस्या, और MoE page इसे मानक तरीक़े से ठीक करती है: एक auxiliary loss जोड़ो जो असमान routing को सज़ा दे। यह काम करता है। यह एक gradient भी जोड़ता है — ऐसा gradient जो बनावट से ही task का gradient नहीं है। आप router से कह रहे हैं कि वह सही होने के बजाय संतुलित हो।
V3 इसकी जगह वह चीज़ रखता है जिसे कोई भी SRE तुरंत पहचान लेगा: एक thermostat.
selection_score = affinity + bias # bias तय करता है कौन चुना जाएगा
gate = affinity # loss को कभी नहीं, इसलिए grad(bias) = 0
bias[expert] -= gamma * sign(load[expert] - mean_load) # हर step के बाद
Expert पर बोझ ज़्यादा, तो bias नीचे। Expert भूखा, तो bias ऊपर। तय क़दम, सिर्फ़ sign. यह selection को हिलाता है और gradient को छूता तक नहीं। मेरी script एक खिलौना दुनिया बनाती है जिसमें token demand Zipf-skewed है — यानी model जो routing चाहता है वह सचमुच असंतुलित है, और balance तथा गुणवत्ता वाक़ई आपस में लड़ते हैं।
true expert demand (Zipf, the routing the model actually wants):
[162 90 73 49 43 35 27 33]
max/min expert occupancy, by step (1.0 = perfectly balanced):
step none aux-loss bias (V3)
0 11.2 11.2 11.2
10 8.7 3.8 8.1
20 6.6 3.2 2.0
40 6.1 3.0 2.2
59 6.1 3.0 2.1
final per-expert token counts (512 tokens, top-1):
none : [165 89 74 51 41 33 27 32]
aux-loss: [121 92 71 56 48 41 42 41]
bias : [80 53 60 74 62 50 43 90]
V3 bias vector: [-3. -1.8 1.2 1.2 3. 3.6 3.6 3. ]
दोनों संतुलन ले आते हैं। अब वह आँकड़ा जो पूरे विचार की जड़ है — balancing तंत्र router के weights में कितना gradient जोड़ता है, और bias हटा देने के बाद router कितनी अच्छी तरह सही expert चुनता है:
gradient norm contributed by the balancing term:
aux-loss : 0.434286 <- fights the routing the model wanted
bias (V3): 0.000000 <- exactly zero, by construction
router accuracy vs the true best expert (bias removed):
no balancing : 0.9590
aux-loss : 0.8848
bias (V3) : 0.9590
बेहतर balance और accuracy में कोई नुक़सान नहीं। auxiliary weight बढ़ाइए तो वही नाकामी मिलेगी जिससे बचने के लिए यह paper बना है — आप वह balance ख़रीदते हैं जो पहले से था, और क़ीमत गुणवत्ता से चुकाते हैं:
BREAK IT: crank the auxiliary loss weight to alpha=6.0
balance max/min : 3.1 (aux 1.50: 3.0, bias: 2.1)
router accuracy : 0.7324 <- balanced, and worse
balancing grad : 1.323678
Thermostat की अपनी नाकामी भी है, और वही जिसका आप अंदाज़ा लगाएँगे:
BREAK IT: bias update rate gamma=8.0 instead of 0.6
max/min occupancy, last 8 steps:
[202. 81.8 224. 202. 63.6 153. 63.4 153. ]
vs gamma=0.6: [2. 1.6 2. 1.6 2. 1.6 1.3 2.1]
final bias vector: [144. 144. 144. 144. 144. 152. 144. 144.]
Node-limited routing: fan-out पर ढक्कन
एक token अपने top-8 experts चाहता है, वे जहाँ भी हों। खुला छोड़ दें तो वह आठ अलग servers को छू सकता है, और हर अतिरिक्त server का मतलब उसी 7168-चौड़े vector की एक और InfiniBand copy, मशीन के सबसे धीमे link पर। ढक्कन यह है: हर node को उसके experts की affinities के योग से अंक दो, सबसे अच्छे M nodes रखो, और top-8 सिर्फ़ उन्हीं में से चुनो। V3 में M = 4.
256 experts over 32 nodes (8 experts per node), top-8, 4096 tokens
uncapped: mean 6.652 nodes/token, max 8
capped : mean 3.748 nodes/token, max 4 (M=4)
InfiniBand bytes, uncapped: 585.9 MB (11.72 ms at 50 GB/s)
InfiniBand bytes, capped : 330.1 MB (6.60 ms at 50 GB/s)
saved: 43.7%
what the cap costs -- experts still reachable per token:
uncapped : 256 of 256 (100.0%)
capped : 32 of 256 (12.5%)
mean overlap with the uncapped choice: 3.62 of 8 experts
यह मुफ़्त नहीं है, और script ठीक-ठीक छापती है कि कितना महँगा है: capped token को उन 8 experts में से 3.62 मिलते हैं जो वह वरना चुनता, और पूरे expert pool का सिर्फ़ आठवाँ हिस्सा ही पहुँच में रहता है। आप model की आज़ादी को wire के समय से बदल रहे हैं। Cap को sweep कीजिए और सौदा देखिए:
M=2: 2.000 nodes/token, 176.1 MB IB, overlap 2.57/8
M=4: 3.748 nodes/token, 330.1 MB IB, overlap 3.62/8
M=8: 5.330 nodes/token, 469.5 MB IB, overlap 5.00/8
M=32: 6.652 nodes/token, 585.9 MB IB, overlap 8.00/8
M को traffic घटाने के लिए नहीं चुना गया — वह तो M=2 करता। इसे इसलिए चुना गया कि उस 3.2:1 अनुपात को देखते हुए IB leg और NVLink leg आकार में इतने पास हों कि एक दूसरे के नीचे छिप जाएँ। Cap और overlap दो design नहीं, एक हैं।
FP8: bytes घटाने का दूसरा आधा हिस्सा
fan-out आधा करने से copies आधी होती हैं। payload आधा करने से हर copy का बोझ आधा होता है। V3 GEMMs को 8-bit floating point में train करता है — अगर आपने quantisation page पढ़ी है तो पहचान जाएँगे कि यह वही तर्क है, एक octave और नीचे। दो formats हैं और अकेले कोई भी काफ़ी नहीं:
format exp man max finite min normal steps per octave
e4m3 4 3 448 1.562e-02 8
e5m2 5 2 57,344 6.104e-05 4
e4m3 के पास precision है, range नहीं। e5m2 के पास range है, precision नहीं। असली tensors को सीधे cast कीजिए और देखिए कौन मरता है:
direct cast, one tensor, no scaling -- relative L2 error:
tensor e4m3 e5m2 e4m3 finite?
weights (N(0,0.02)) 0.310877 0.051356 True
activation (N(0,1)) 0.027098 0.053512 True
act+outlier (x1500) inf 0.024086 False
grads (N(0,1e-5)) 1.000000 1.000000 True
which tensor dies first: the one with the outlier.
max |activation| = 1,500.0 vs e4m3 max 448
values that became inf: 1
gradients underflow e4m3's min normal: 4096 of 4096 -> 0
4096 में से एक value 448 से ऊपर गई और पूरा tensor गया। प्रति tensor एक FP32 scale उसे वापस ले आता है — पर वह scale सबसे तेज़ आवाज़ वाली value तय करती है, और क़ीमत चुकाते हैं चुपचाप बैठे छोटे मान। छोटे blocks, ज़्यादा scales, कम मरी हुई values:
direct cast, no scale : inf <- overflowed, the tensor is gone
one FP32 scale : 0.001151 finite again
4096 activations, N(0,1), with four values of 3000 dropped in.
Error measured on the 4092 ORDINARY values only:
block err on quiet values dead (rounded to 0) bits/value
4096 0.031677 343 8.008
512 0.029651 184 8.062
128 0.026348 38 8.250
32 0.024253 12 9.000
यही V3 की fine-grained quantisation है: activations के लिए 1×128 tiles, weights के लिए 128×128. Format बेहतर नहीं हुआ। Blocks छोटे हुए। Report इसे FP32 accumulation और चौड़े master weights तथा optimiser state के साथ जोड़ती है, क्योंकि जहाँ 8 bits नाकाफ़ी हैं, वही जगहें हैं जहाँ त्रुटि जमा होती है। इससे क्या मिलता है:
one activation tile, 4096 tokens x 7168:
BF16 58.72 MB
FP8 (1x128 scales) 30.28 MB
all-to-all bytes/token: BF16 dispatch 344,064 B ->
FP8 dispatch 258,048 B (25.0% less)
Multi-token prediction: एक forward pass, दो targets
एक सामान्य language model को हर position पर एक ही training signal मिलता है: अगला token बताओ, जैसा GPT page पर है। V3 एक दूसरा head जोड़ता है जो उसके बाद वाला token बताता है, क्रम से, पूरी causal शृंखला बनाए रखते हुए। वही forward pass, दुगुने targets.
micro-batch: 4096 positions
MTP depth 0 (1 head): 4,096 prediction targets per forward pass (1.0x)
MTP depth 1 (2 heads): 8,192 prediction targets per forward pass (2.0x)
MTP depth 2 (3 heads): 12,288 prediction targets per forward pass (3.0x)
what each head predicts, for the sentence positions 0..4:
pos 0 (the ) -> head 1: cat head 2: ate
pos 1 (cat ) -> head 1: ate head 2: the
pos 2 (ate ) -> head 1: the head 2: food
V3 depth 1 इस्तेमाल करता है — एक अतिरिक्त head. यह एक training objective है; सामान्य inference के लिए वह अतिरिक्त head हटा दिया जाता है। पर आप उसे रख भी सकते हैं, और तब वह लगभग मुफ़्त का draft model है — यानी speculative decoding, जिसमें drafter target model के भीतर ही बैठा है। Report दूसरे token की acceptance 85–90% बताती है और इस तरह इस्तेमाल करने पर लगभग 1.8× decoding throughput.
expected tokens accepted per decoding step = 1 + p(second accepted):
acceptance 0.85 -> 1.85 tokens/step (1.85x, before the extra head's own cost)
acceptance 0.90 -> 1.90 tokens/step (1.90x, before the extra head's own cost)
1 + p वाला गणित मेरा है, उनका नहीं, और यह verification की लागत नहीं गिनता। 85–90% और 1.8× report के आँकड़े हैं।
Communication को compute से overlap करना
आप all-to-all मिटा नहीं सकते। आप उसे तब करा सकते हैं जब GPU कुछ और कर रहा हो — वही async call जो आप बिना सोचे लिख देते हैं। मेरी script एक 1F1B pipeline simulate करती है, 8 stages, 32 microbatches, और पूछती है कि device कर क्या रहा है। यह एक schedule की simulation है, DeepSeek की माप नहीं।
pipeline: 8 stages, 32 microbatches, compute 1.0, all-to-all 0.8 per microbatch
schedule makespan device busy bubble
no overlap 70.20 45.6% 54.4%
comm hidden 39.00 82.1% 17.9%
wall-clock penalty for not overlapping: 1.80x
illustration only: a 56.7-day run with this shape becomes 102.1 days
दूसरा column ही घूरने लायक़ है। एक बार communication छिप गया, तो उसे और महँगा करना तब तक मायने नहीं रखता जब तक वह compute से आगे न निकल जाए:
comm bubble (no overlap) bubble (overlapped) speedup
0.2 31.6% 17.9% 1.20x
0.5 45.3% 17.9% 1.50x
0.8 54.4% 17.9% 1.80x
1.2 62.7% 31.6% 1.83x
2.0 72.6% 59.0% 1.50x
"abstraction layer के नीचे जाना" का असल मतलब
Report में तीन बातें हैं जिन्हें ज़्यादातर लेख छोड़ देते हैं, और वही वजह हैं कि ऊपर के तीनों उपाय ख़्वाब न रहकर सचमुच हो पाए:
| उन्होंने क्या किया | क्यों |
|---|---|
| communication kernels के लिए PTX लिखा, यानी CUDA से नीचे NVIDIA की assembly-स्तर की layer | dispatch/combine kernels को हाथ से tune करना ज़रूरी था. Inline asm, क्योंकि compiler वह नहीं करता |
| हर GPU के streaming multiprocessors का एक हिस्सा communication के लिए आरक्षित रखा | Comm और compute सचमुच साथ-साथ चलते हैं. आप cores का budget बनाते हैं, जैसे thread pool pin करते हैं |
| Tensor parallelism से पूरी तरह बचे | यह सबसे ज़्यादा communication माँगने वाला parallelism है — यह हर layer के भीतर बात करता है. सँकरे link पर सबसे पहले यही हटता है |
Multi-head latent attention भी उन्हीं का है, DeepSeek-V2 से: यह key-value cache को प्रति token एक low-rank latent vector में दबा देता है, जिससे long-context serving memory की समस्या नहीं रह जाती। उसका अपना पेज है — multi-head latent attention, जो KV cache page की byte-गिनती पर खड़ा है — इसलिए यहाँ मैंने जानबूझकर उसे दोबारा नहीं निकाला।
वह आँकड़ा जो सब ग़लत बताते हैं
आपसे $5.6M के बारे में पूछा जाएगा। एक बार ख़ुद भाग देकर देख लीजिए, फिर आप उसे कभी लापरवाही से नहीं दोहराएँगे:
final pre-training GPU-hours (report) : 2,788,000 H800-hours
assumed rental price (report's own) : $2.00 per GPU-hour
product : $5.576M
on 2048 GPUs that is 56.7 days of wall clock
tokens: 14.8e12 over 671e9 params, 37e9 active per token
tokens per active parameter : 400
approx forward+backward FLOPs (6*N*D) : 3285.6 ZFLOP
implied MFU against BF16 peak : 33.1%
against the FP8 peak they actually used: 16.5%
प्रति active parameter 400 tokens वाली बात scaling laws page के साथ रखकर दोबारा देखने लायक़ है — यह Chinchilla-optimal से कहीं आगे है, और यही आप तब करते हैं जब इरादा model को serve करने का हो, न कि compute-optimal बहस जीतने का।
The code
Pure NumPy. No torch, no GPU, no training run. Everything is arithmetic: bytes counted, formats simulated, schedules stepped. Every function under 20 lines.
Download the full script — 718 lines plus 7 runnable demos. Then:
python3 deepseek-v3_from_scratch.py
def moe_flops_per_token():
"""One token through top-8 SwiGLU experts. 3 matrices, 2 flops per MAC."""
per_expert = 3 * 2 * D_MODEL * EXPERT_HIDDEN
return TOPK_ROUTED * per_expert
def all_to_all_bytes_per_token(nodes_touched, dispatch_bytes, combine_bytes):
"""Bytes a single token pushes across each link type in one MoE layer.
Dispatch: the hidden vector goes OUT to every node holding a chosen
expert -- one InfiniBand copy per node, then NVLink fans it out to the
GPUs inside that node. Combine: the expert outputs come back the same
way. V3 dispatches in FP8 and combines in BF16, so the two directions
are not the same size.
"""
ib = nodes_touched * D_MODEL * (dispatch_bytes + combine_bytes)
nvl = TOPK_ROUTED * D_MODEL * (dispatch_bytes + combine_bytes)
return ib, nvl
def seconds(byte_count, bandwidth):
"""Bytes / bandwidth. This is the whole 'timing' model. No GPU was run."""
return byte_count / bandwidth
def route(X, W_g, bias=None, k=1):
"""Affinity scores, then top-k selection. Bias shifts SELECTION only."""
scores = X @ W_g # (n_tokens, n_experts)
probs = softmax(scores) # what the model believes
sel_scores = scores if bias is None else scores + bias
chosen = np.argsort(-sel_scores, axis=1)[:, :k]
return probs, chosen
def bias_balance_step(bias, chosen, n_experts, gamma):
"""V3's replacement, in three lines. Auxiliary-loss-free balancing.
Starved expert -> bias up. Hot expert -> bias down. Fixed step size,
sign only, like a thermostat. Never touches the loss, so it contributes
NO gradient to W_g -- the router keeps learning what it wanted to learn.
"""
load = load_counts(chosen, n_experts)
err = load - load.mean() # + means overloaded
return bias - gamma * np.sign(err)
def node_limited_topk(scores, n_nodes, k, m_cap):
"""Restrict candidates to the m_cap best nodes, then take top-k."""
n_tokens, n_experts = scores.shape
node_id = node_of_expert(n_experts, n_nodes)
per_node = n_experts // n_nodes
node_score = scores.reshape(n_tokens, n_nodes, per_node).sum(axis=2)
keep = np.argsort(-node_score, axis=1)[:, :m_cap] # (n_tokens, m_cap)
allowed = np.zeros((n_tokens, n_nodes), dtype=bool)
np.put_along_axis(allowed, keep, True, axis=1)
masked = np.where(allowed[:, node_id], scores, -np.inf)
return np.argsort(-masked, axis=1)[:, :k]
fp8_cast, fp8_blockwise and pipeline_bubble
are the same shape and live in the script — the FP8 grid is np.frexp,
round the mantissa, overflow to inf; the pipeline is a dozen lines
of nested loop over stages and microbatches.
Now break it
This is the part that actually makes it stick. Reading won't do it. You debug for a living — reverse-engineering from broken behaviour is your native mode, and every row below is one number in the script.
| Break this | Watch for |
|---|---|
In demo_2_balancing, set train("aux", alpha=6.0) as the main run | Balance stops improving at max/min 3.1 while router accuracy falls 0.9590 → 0.7324. You bought nothing and paid for it. |
In demo_2_balancing, set gamma=8.0 on the bias run | Occupancy stops settling and oscillates: [202. 81.8 224. 202. 63.6 …] instead of [2. 1.6 2. 1.6 …]. The controller's gain is too high. |
In demo_4_fp8, call fp8_cast(a, "e4m3") instead of fp8_blockwise | The activation tensor with the 1500 outlier goes to inf — relative error inf, one value overflowed, whole tensor dead. Gradients at N(0,1e-5) die the other way: 4096 of 4096 flushed to zero. |
In demo_3_node_limited_routing, set NODE_CAP_M = 32 (every node) | Cross-node traffic climbs from 330.1 MB to 585.9 MB, 6.60 ms → 11.72 ms at 50 GB/s. Tokens reach 8.00/8 of their preferred experts, and the IB leg no longer fits under the NVLink leg. |
In pipeline_bubble, pass overlap=False | Bubble 17.9% → 54.4%, makespan 39.00 → 70.20. A 56.7-day run becomes 102.1 days at that ratio. |
In all_to_all_bytes_per_token, pass dispatch_bytes=2 (BF16, not FP8) | All-to-all bytes per token go 258,048 → 344,064, a 33% increase on the link that was already the bottleneck. |
On the maths anxiety
There is almost no maths on this page, and that is not me being kind. The spine of DeepSeek-V3 is division: bytes over bandwidth, FLOPs over throughput, compare. If you have ever sized a queue, worked out whether a job is IO-bound, or argued about payload compression, you already own the reasoning.
The two places actual maths appears, you can take on trust. The auxiliary loss has a gradient and a bias term does not — that's a one-line consequence of what "in the loss" means, and my script prints the two norms rather than deriving them. FP8's range and precision come from counting exponent and mantissa bits, which is a spec-sheet lookup, not analysis.
What this page does not give you: the actual convergence behaviour of FP8 training, the real profile of their kernels, and anything about post-training. Those need a cluster, and no amount of NumPy substitutes for one. What it gives you is the ability to say why each choice exists, which is the interview question.
Where to go next
This page gave you one angle: the interconnect as the cause, everything else as effect. That deliberately underplays the model as a model. These three fill in the rest.
-
DeepSeek-V3 Technical Report
Read the infrastructure section properly — it is unusually candid about what was hard, and it is where the PTX, the reserved SMs and the FP8 framework live. The architecture section gives you MLA and the balancing bias. The evaluation tables you can skim.
-
Fire-Flyer AI-HPC: A Cost-Effective Software-Hardware Co-Design
The same team, four months earlier, describing the machine V3 was trained on: network topology, storage, scheduling, failure handling. This page treats the interconnect as a constraint that arrived from outside; this paper is them designing around it deliberately, at the level of cabling.
-
How to Scale Your Model
The rigorous version of my one formula. Roofline analysis, every parallelism strategy costed in bytes, and the collective operations (all-gather, reduce-scatter, all-to-all) done properly rather than hand-counted as I did. Go here when my table stops being enough.
-
DeepSeek V3 and the actual cost of training frontier AI models
What the $5.6M does and doesn't cover, written by someone who has run these budgets. Read it right after my last section, before you repeat the number to anyone.