Skip to content

Add blog post: Running a big LLM across multiple GPUs with vLLM - #265

Draft
saiyam1814 wants to merge 15 commits into
mainfrom
blog/two-gpu-vllm
Draft

Add blog post: Running a big LLM across multiple GPUs with vLLM#265
saiyam1814 wants to merge 15 commits into
mainfrom
blog/two-gpu-vllm

Conversation

@saiyam1814

@saiyam1814 saiyam1814 commented Aug 18, 2026

Copy link
Copy Markdown
Member

What this is

A new blog post answering a reader question: someone with two A40s (45 GiB usable each) wanted to serve a BF16 model split across both cards, and asked how the weight splitting actually works.

The post covers:

  • The memory math that tells you whether a model fits before you download 60 GB
  • What tensor parallelism does to each weight matrix, column-parallel then row-parallel, and why that costs exactly two all-reduces per layer
  • The exact vLLM commands with real terminal output
  • Measured TP vs PP on a pair of cards with no NVLink
  • A40-specific catches, including the FP8 compute-capability limit

Where the numbers come from

Everything measured on two RTX PRO 6000 Blackwell cards held to a 40.47 GiB per-card budget (--gpu-memory-utilization 0.426) so the memory behaviour matches a 45 GiB A40 running at 0.90. Model is Qwen3-32B in BF16, a 61.02 GiB checkpoint, on vLLM 0.27.1.

Headline results:

TP=2 PP=2
tok/s at concurrency 1 36.41 21.00
tok/s at concurrency 32 496.60 487.56
median TTFT at 32 3892 ms 2468 ms
KV cache 67,296 tokens 56,640 tokens

Worth flagging for reviewers: vLLM's docs recommend pipeline parallelism over tensor parallelism on boxes without NVLink for "higher throughput", and that did not reproduce here. The post reports it as a miss rather than bending the result, and explains why the crossover may sit differently on Gen4 A40s.

New animations

Three CSS-only animations following the existing series pattern (no hooks, no use client, theme variables, prefers-reduced-motion guards), registered in lib/markdown.js:

  • {{two-gpu-tensor-split-animation}} the split and the all-reduce
  • {{two-gpu-memory-fit-animation}} 61.02 GiB against one card, then two
  • {{two-gpu-tp-vs-pp-animation}} the tradeoff plus measured results

Verified rendering in both light and dark mode.

Notes for review

  • draft: true had to be removed so the post renders on the Cloudflare preview: drafts are
    filtered out of production builds (INCLUDE_DRAFTS is false when NODE_ENV is production), so
    the preview 404s while the flag is set. This PR stays in draft as the gate, so nothing
    publishes until it is marked ready and merged. Re-add the flag if you would rather hold it after merge.
  • The TP-vs-PP animation repeats four numbers that also appear in the prose table just below it. Happy to drop either.
  • Cover is generated by scripts/gen-two-gpu-vllm-cover.mjs and converted with rsvg-convert -w 1200 -h 630.

Answers a reader question about splitting a BF16 model across two A40s:
how tensor parallelism partitions each layer, the memory math for whether
it fits, and measured TP vs PP numbers on a pair of cards with no NVLink.

All figures come from a real run (vLLM 0.27.1, Qwen3-32B BF16) on two
RTX PRO 6000 cards held to a 40.47 GiB per-card budget to match a 45 GiB
A40 at --gpu-memory-utilization 0.90.

Adds three CSS animations following the existing series pattern:
- two-gpu-tensor-split-animation: column/row-parallel split, one all-reduce
- two-gpu-memory-fit-animation: 61.02 GiB against one card, then two
- two-gpu-tp-vs-pp-animation: the TP/PP tradeoff plus measured results

Marked draft: true pending review.

Signed-off-by: Saiyam Pathak <saiyam911@gmail.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 18, 2026

Copy link
Copy Markdown

Deploying website with  Cloudflare Pages  Cloudflare Pages

Latest commit: 00b7452
Status: ✅  Deploy successful!
Preview URL: https://b4635e1a.website-dab.pages.dev
Branch Preview URL: https://blog-two-gpu-vllm.website-dab.pages.dev

View logs

Drafts are filtered out of production builds (INCLUDE_DRAFTS is false when
NODE_ENV is production), so the preview deploy 404s on the post while the
flag is set. Removing it makes the PR preview reviewable. The PR itself
stays in draft, so nothing publishes until it is marked ready and merged.

Signed-off-by: Saiyam Pathak <saiyam911@gmail.com>
Replaces the two-GPU A40 post. The old version simulated a 2-GPU split by
capping memory on larger cards, which meant the central claim was emulated
rather than measured. This version uses a model that genuinely does not fit:
Qwen3-235B-A22B-Instruct-2507-FP8, 221 GiB on disk, 2.3x too big for one
96GB card.

Reframed away from A40 specifics to the general question of how a big model
is spread over several GPUs and how inference works once it is. Byline is now
Shubham and Saiyam. Rewritten in plain English throughout, with every flag
and every line of the docker command explained.

All three splitting modes measured on the same 4 GPUs:
- TP=4:      17.14 ms/token, 503.68 tok/s at 32 concurrent, 621,392 KV tokens
- TP=4 + EP: 18.83 ms/token, 470.93 tok/s, 623,696 KV tokens
- PP=4:      21.19 ms/token, 296.48 tok/s, 555,680 KV tokens, best TTFT

Plus three real failure modes with their actual error text: an invalid
tensor-parallel size, a genuine CUDA OOM at 2 GPUs, and a DeepGEMM
"Unknown SF transformation" crash on sm_120 that needs VLLM_USE_DEEP_GEMM=0.

Adds Part 1 on downloading and on-disk storage (safetensors shards, the HF
cache blob layout, FP8 block scales) and Part 3 on what inference actually
does (prefill versus decode, continuous batching, why capacity is set by the
KV cache). Includes the disk-pressure hazard that evicted pods on our own
test node.

Four CSS animations replace the previous three, following the existing
series pattern: three-ways-to-split, tensor split across 4 GPUs, expert
routing, and memory fit on 1 / 2 / 4 GPUs.

Signed-off-by: Saiyam Pathak <saiyam911@gmail.com>
@saiyam1814 saiyam1814 changed the title Add blog post: Running a single LLM across two GPUs with vLLM Add blog post: Running a big LLM across multiple GPUs with vLLM Aug 18, 2026
saiyam1814 and others added 9 commits August 18, 2026 17:15
A reader asked whether the shard count can be changed at download time and
whether there is a standard. Adds two subsections to Part 1: shards are fixed
by the publisher and recorded in model.safetensors.index.json, re-sharding is
a local save_pretrained(max_shard_size=...) operation, and the Hub's <200GB
recommendation plus 500GB hard limit explain why publishers land around
5-10GB. This model uses a 10GB cap: 23 shards of exactly 10.00 GB plus a
6.45 GB remainder. Also notes that shard count does not affect serving,
because safetensors are memory-mapped.

Signed-off-by: Saiyam Pathak <saiyam911@gmail.com>
Signed-off-by: Saiyam Pathak <saiyam911@gmail.com>
…tion-focused and theory-focused readers each get a direct path through it
…aying for theory

The two-track structure asked every reader to route themselves through a
7,100-word post before doing anything. Readers who just need a big model
serving now get a single 4,500-word path with no detours, and the mechanics
move to a second post for readers who want them.

The largest section, Step 1, led with safetensors internals and shard-size
conventions and buried the disk trap that actually took down our Kubernetes
node; the disk check now opens the step. All twelve links into the deep dive
became plain-text forward references rather than links to a URL that does not
exist yet.

Adds a cake-layers diagram at the point in Step 3 where the reader has to pick
a split, since that decision is the one place the runbook genuinely needed a
visual. The three heavier animations go with part two.
Shubham's eight-step runbook is the right shape and stays as the spine.
But the cut removed both benchmark tables while keeping the verdict that
rests on them, so three headline percentages had nothing behind them, and
it deferred the post's central question to a part two we have not scoped.
This makes the post stand alone.

Restored, placed where the reader is making a decision rather than
front-loaded as theory:

- Step 3 gains the restaurant-kitchen analogy, what a tensor-parallel
  split costs (2 all-reduces per layer, 94 layers, 188 per token), and
  why a 235B MoE is only 22B of arithmetic. The three animations that
  were left orphaned by the cut now have homes here.
- Step 7 gains the prefill-versus-decode split, which is the only thing
  that explains why pipeline parallelism wins first-token latency while
  tensor parallelism wins throughput, then both measurement tables, the
  28.71 tok/s outlier we discarded and why, and the NVLink caveat.

The TP throughput figure was re-measured at --num-prompts 640 while PP
and EP were measured at 128, so the speed table now says so. TP came out
at 507.09 against 503.68 at the smaller scale, a 0.68% difference, so the
comparison holds at both.

Fixes: the startup-log section told readers to grep for "Available KV
cache memory", which vLLM 0.27.1 does not print and which appears nowhere
in our own pasted log. Model loading time said 45 seconds in prose and
48.5 in the log 120 lines below. "Those 10 GB boundaries" referred to a
figure removed with the shard section. Two flag-table rows promised tests
that had been deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous cake-layers.png was an AI-generated illustration whose layer
axis had a duplicated tick: it read 1, 2, 3, 4, 4, 5, 6, 7, 8, 9, 10, so
eleven marks for ten layers. Text was baked into the raster, so it could
not be corrected without regenerating the whole image.

scripts/gen-multi-gpu-cake-diagram.mjs now emits it the way every other
diagram on the site is built, as hand-rolled SVG in the Excalidraw style
of gen-hami-diagrams.mjs, with the layer count as a constant so the axis
cannot drift from the labels again. 383 KB of raster becomes 18 KB of SVG
and a 116 KB PNG for the markdown reference.
The topology matrix was authored as a markdown table but wrapped in a
```bash fence, so the first technical artifact in the post rendered as
raw text: literal `| :---: |` separators and `**GPU0**` asterisks. It is
now a real table, with the legend trimmed to the codes that matter.

The glossary sentence was hardcoded in the markdown, but GlossaryNote
already renders it above the body for anything tagged vllm or llm, so
readers saw it twice. Removed the inline copy, which also drops an
absolute link the rest of the site writes as a relative one.

`HF_HOME` prose pointed at the `hub/` directory the tree diagram is
rooted at, while the Step 5 command correctly sets its parent. Following
the prose sends the libraries looking in `hub/hub/` and re-downloads
236 GB. Spelled out which one it is and why.

Also from the review:

- "the largest data-centre GPUs top out well below 236 GB" is false;
  B300 and MI355X both carry 288 GB. Narrowed to the box we ran on.
- "Powers of two are the safe habit" pointed at -tp 8, the exact
  configuration Step 3 spends a section arguing against on this model.
- The copy-pasteable benchmark carried --seed 42 with prefix caching on,
  the trap the post warns about 80 lines later. Added the flag.
- can_device_access_peer returns True on a SYS pair, which read as a
  contradiction. It reports capability, not speed; said so.
- PP's throughput deficit was blamed on too few requests in flight, but
  32 over 4 stages is 8 each, which the post's own analogy calls full.
  Reattributed to uneven stages, which the memory table already shows.
- The flags table described three flags the command does not set and
  omitted VLLM_USE_DEEP_GEMM, without which the server does not boot.
- Step 2 told readers to run the fit check before downloading while
  sitting after the download step.
- datePublished predated every pasted log by three days.
- The memory-fit animation printed the log string Step 6 now explains
  vLLM 0.27.1 does not emit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@saiyam1814

Copy link
Copy Markdown
Member Author

Three commits on top of the runbook cut

@shkatara's eight-step restructure is the base and stays. The disk trap leading Step 1, merging the divisibility rules into the split choice, the real pasted logs, the --max-num-seqs queueing explanation and the 640-prompt re-run are all kept. He also caught a factual error in the original flags table (-pp 2 where we actually tested -pp 4).

1. Restore the evidence and the why, drop the part-two promises

The cut removed both benchmark tables but kept the verdict resting on them, so "70% faster", "15%", and "7%" had nothing behind them. It also deferred the post's central question to a part two we have not scoped.

Restored, placed where the reader is deciding rather than front-loaded:

  • Step 3: the restaurant-kitchen analogy, what a tensor-parallel split costs (2 all-reduces per layer x 94 layers = 188 per token), and why a 235B MoE is only 22B of arithmetic.
  • Step 7: prefill vs decode with its comparison table, both measurement tables, the 28.71 tok/s outlier and why we threw it out, and the NVLink caveat.

The three animations orphaned by the cut now have homes. Four animations, zero orphans.

On the benchmark scales: TP was re-measured at --num-prompts 640, PP and EP at 128. TP came out at 507.09 tok/s against 503.68, and 3,211 ms median TTFT against 3,233 ms. Both inside 1%, so the comparison holds and the post now says so explicitly. I dropped the benchmark-duration row, which was the only absolute measure and therefore the only genuinely incomparable one.

Arithmetic corrected: 621,392 to 555,680 tokens is a 10.6% drop, not 11.8% (that figure divided by the post-drop value). 87,899 - 84,283 = 3,616 MiB, which is 3.5 GiB, not 3.6 GB.

2. Regenerate the cake diagram

The previous PNG's layer axis read 1, 2, 3, 4, 4, 5, 6, 7, 8, 9, 10, eleven marks for ten layers, with the text baked into the raster. Now generated by scripts/gen-multi-gpu-cake-diagram.mjs as SVG in the same Excalidraw style as gen-hami-diagrams.mjs, with the layer count as a constant so the axis cannot drift from the labels again. 383 KB to 18 KB of SVG plus a 116 KB PNG.

3. Review fixes

  • The nvidia-smi topo -m output was authored as a markdown table but wrapped in a ```bash fence, so the first technical artifact in the post rendered as literal | :---: | separators and **GPU0** asterisks. Now a real table.
  • The glossary sentence was hardcoded in the markdown while GlossaryNote already renders it for anything tagged vllm/llm. Readers saw it twice.
  • HF_HOME prose pointed at the hub/ directory; the command correctly sets its parent. Following the prose sends the libraries to hub/hub/ and re-downloads 236 GB.
  • "the largest data-centre GPUs top out well below 236 GB" is false. B300 and MI355X both carry 288 GB.
  • "Powers of two are the safe habit" pointed at -tp 8, the configuration Step 3 argues against on this model.
  • The copy-pasteable benchmark carried --seed 42 with prefix caching on, the exact trap warned about 80 lines later.
  • can_device_access_peer returning True on a SYS pair read as contradicting the topology. It reports capability, not speed.
  • PP's throughput deficit was blamed on too few requests in flight, but 32 over 4 stages is 8 each, which the post's own analogy calls a full pipeline. Reattributed to uneven stages, which the memory table already shows.
  • The flags table described three flags the command does not set and omitted VLLM_USE_DEEP_GEMM, without which the server does not boot.
  • Step 2 told readers to run the fit check before downloading while sitting after the download step.
  • datePublished predated every pasted log by three days.
  • The memory-fit animation printed the log string Step 6 now explains vLLM 0.27.1 does not emit.

Notes

Word count: 7,791 original, 6,527 after the cut, 8,687 now. The growth is the two tables plus the two explanations that make them mean something, on @shkatara's spine rather than the original twelve parts.

Builds clean at 619 pages. All four animations are server components with CSS keyframes, so they ship zero client JS.

Still open: the post is still a draft PR, and datePublished is set to 2026-08-31, so bump it if this merges later.

saiyam1814 and others added 3 commits August 31, 2026 17:17
…a is

Review feedback from Shubham: a runbook should not stop to teach Megatron
matrix partitioning, and the KV cache formula was unreadable.

The formula `2 x layers x kv_heads x head_dim x bytes_per_number` has two
different 2s in it and labelled neither, so the first one reads as "two
blocks per layer, attention and mlp". It is not: it is one key and one
value per token, which is where the KV in KV cache comes from. Every term
now has a row saying what it is and which config field it comes from.

Cut, because they were teaching rather than running:

- The Megatron column-and-row derivation, which introduced "the first
  matrix" and "the second matrix" without ever saying what they were.
  What survives is the one fact Step 7 needs: an all-reduce twice per
  layer, 188 per token, over PCIe on this box, and the tax is what the
  benchmark measures.
- The expert-routing deep dive. "235B-A22B" is already unpacked in The
  machine and the model, so the second pass was redundant.
- The two-phase prefill and decode section with its own table, now one
  paragraph covering why the two metrics move in opposite directions.

Also merged Step 3's six bullets into three: the analogy and the flag
list were covering the same three modes twice.

The measurement tables stay. They are evidence, not theory, and the
verdict is unsupported without them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Shubham asked for the original image back. It was never removed, it was
redrawn as an SVG diagram, and the redraw was the wrong call: the reason
to touch it at all was a single bad glyph, not the illustration.

The left axis read 1, 2, 3, 4, 4, 5, 6, 7, 8, 9, 10, eleven labels for
ten layers, with the extra 4 struck through, plus a faint smudge above
the 10. Both are painted out against the surrounding background, so the
illustration is otherwise byte-for-byte the original.

Drops cake-layers.svg and scripts/gen-multi-gpu-cake-diagram.mjs, which
generated the replacement nobody wanted. Both are recoverable from
abb7e00 if the illustration ever needs a vector counterpart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three of the four gpu_worker.py lines were byte-identical to the first
apart from worker index and pid, so they cost 200 words to say nothing.
One line plus a sentence noting the other three match, which is the
actual point, since agreeing exactly is what proves the split is even.

Both benchmark dumps reprinted the same preamble and the same twenty
metric lines when the post only ever cites six of them. Kept those,
elided the rest.

43 min read to 41. No evidence removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants