cd /news/ai-infrastructure/serving-gemma-4-on-an-amd-mi300x-wha… Β· home β€Ί topics β€Ί ai-infrastructure β€Ί article
[ARTICLE Β· art-133084] src=dev.to β†— pub= topic=ai-infrastructure verified=true sentiment=↑ positive

Serving Gemma 4 on an AMD MI300X: What $1.99 an Hour Buys

A developer published a step-by-step guide and open-source MCP toolkit for deploying Google's Gemma 4 E2B model to a single AMD Instinct MI300X GPU rented through AMD Developer Cloud at $1.99 per hour. The vLLM-based setup reached 305 tokens per second on a single stream and 10,284 tokens per second across 64 streams, or roughly 2,713 tokens per second per dollar-hour. The 21-tool MCP server runs on a workstation with no local AMD hardware, reaching the remote GPU over SSH and the DigitalOcean v2 API.

by read10 min views1 publishedSep 17, 2026

This article provides a step by step deployment guide for Gemma 4 E2B to an AMD Instinct MI300X hosted GPU enabled system. A suite of Python MCP tools is built to simplify management of the vLLM hosted deployment. The card is reached through AMD Developer Cloud, which is DigitalOcean underneath, and the workstation driving it has no AMD GPU in it at all.

github.com/xbill9/gemma4-dev/tree/main/gpu-vllm-mi300x-2b

| Model | google/gemma-4-E2B-it , reference bf16 release | | Hardware | 1x AMD Instinct MI300X, gfx942 , CDNA 3, 191.7 GiB HBM3 | | Host | gpu-mi300x1-192gb-devcloud β€” 20 vCPU, 240 GB RAM, 720 GB disk, regionatl1 | | Control plane | AMD Developer Cloud / DigitalOcean v2 API β€” no gcloud , no EC2 | | Software | vllm/vllm-openai-rocm:nightly-rocm100 , vLLM0.3.1.dev3+g0bfc7a15d | | Rate | $1.99 per hour , on-demand | | Result | 305 tok/s single stream,10,284 tok/s at 64 streams,2,713 tok/s per dollar-hour |

The card is a DigitalOcean GPU droplet reached through AMD Developer Cloud (devcloud.amd.com) β€” same v2 API, same droplet ids, token from the My AMD Team account. Creating and destroying it are console actions, deliberately: both are dollar-per-hour decisions and neither belongs in a tool an agent can call.

Everything after creation is scripted. The droplet carries a tag, every tool is scoped to that tag, and nothing in the toolkit can touch an instance that does not have it.

DIGITALOCEAN_ACCESS_TOKEN in a mode 0600 .env beside the server/dev/kfd and /dev/dri present rocm-smi, amd-smi, rocminfo and hipcc do not exist locally and never will. A ROCm command in a local shell is a bug, not a check. Every reading in this article came back over SSH or through the DigitalOcean API.

That constraint shapes the whole toolkit. The MCP server runs on the workstation, holds no device, and reaches the hardware the same way you would by hand.

One file, server.py, exposes 21 tools over stdio: droplet lifecycle, image checks, deploy, logs, benchmark cells, and the SRE probes below. Every subprocess goes through one run_command(cmd: list[str]) helper using asyncio.create_subprocess_exec β€” never a shell.

$ grep -c "^@mcp.tool" server.py
21
bash
$ droplet_status
πŸ“‘ debian-gpu-mi300x1-192gb-devcloud-atl1 (601418522)

- status: `active`
- size: `gpu-mi300x1-192gb-devcloud` β€” 20 vCPU, 240 GB RAM, 720 GB disk
- region: atl1
- cost: $1.99/hr β€” billed while powered off, too

The rate comes from the DigitalOcean v2 API's own droplet.size.price_hourly field, read at the time of the run, not from a pricing page.

$ gpu_status
βœ… GPU reporting on `debian-gpu-mi300x1-192gb-devcloud-atl1`.

| Card | Product | GPU use % | VRAM used % |
| card0 | Aqua Vanjaram [Instinct MI300X VF] | 0 | 0 |

It presents as an SR-IOV virtual function, which looks like a partition and is not one: all 304 compute units and the whole 191.7 GiB are there. Neither rocm-smi nor amd-smi sets a useful exit code, so the tool parses the output and ignores the status code entirely.

vllm/vllm-openai-rocm:nightly-rocm100. Read the version out of the running container rather than trusting the tag, because a nightly tag moves under you:

$ docker exec vllm python3 -c "import vllm;print(vllm.__version__)"
0.3.1.dev3+g0bfc7a15d

Not every ROCm vLLM image can load this checkpoint β€” Gemma 4 runs 256-wide heads on its sliding-attention layers and 512 on its full-attention ones, and an image whose vLLM predates that split cannot parse the config. The measured comparison of three images is in the companion article. Here the practical rule is enough: pin what you measured, and check a new image before you adopt it.

$ deploy_vllm
βœ… Started `vllm` serving `google/gemma-4-E2B-it`.

- image: `vllm/vllm-openai-rocm:nightly-rocm100`
- context: 32768, gpu-memory-utilization 0.90
- multimodal: `{"image": 4, "audio": 0}`

The serve arguments, in full:

vllm serve google/gemma-4-E2B-it --host 0.0.0.0 --port 8000 \
  --max-model-len 32768 --gpu-memory-utilization 0.90 \
  --enable-auto-tool-choice --reasoning-parser gemma4 --tool-call-parser gemma4 \
  --chat-template /app/vllm/examples/tool_chat_template_gemma4.jinja \
  --limit-mm-per-prompt '{"image": 4, "audio": 0}' --async-scheduling

audio is 0 on purpose. E2B has a conformer audio encoder and no ROCm vLLM image ships the vllm[audio] extras, so a non-zero limit would allocate encoder memory for a path that cannot be used.

docker run returns in about a second. The endpoint does not answer for another two and a half minutes, while weights load, torch.compile runs and graphs are captured.

READY after 160s
{"object":"list","data":[{"id":"google/gemma-4-E2B-it","max_model_len":32768,...}]}

This is why serving_status reports the container and the endpoint as two separate facts. A running container is not a serving model, and conflating them turns a normal boot into a phantom hang.

$ serving_status
βœ… container: Up 3 minutes | endpoint: answering on `127.0.0.1:8000`
   served: `google/gemma-4-E2B-it`

A flag being accepted is not evidence it did anything, so every modality is probed with a request whose correct answer is known in advance. The vision probe is a generated checkerboard rather than a fetched photo, so the expected answer is a fact about the image and not a caption.

$ verify_capabilities
βœ… 4/4 capabilities verified.

| text         | βœ… | The AMD MI300X is based on the CDNA 3 architecture. |
| thinking     | βœ… | 1404 chars, 447 reasoning tokens |
| tool calling | βœ… | tool_calls β†’ get_weather{"city": "Reykjavik"} |
| vision       | βœ… | alternating bright red and royal blue squares |

πŸ“‘ Audio is not probed β€” no ROCm vLLM image ships the `vllm[audio]` extras.

The boot log is worth reading before sizing anything, because the engine states its own arithmetic:

Available KV cache memory: 155.04 GiB
GPU KV cache size: 9,026,017 tokens
Maximum concurrency for 32,768 tokens per request: 275.45x

Nine million tokens of KV on one card. Hold that number β€” it decides which of the two ceilings below you actually hit.

Four concurrencies against four context lengths, 128 output tokens throughout, three repeats per cell, load generated by vLLM's own bench client in a second container with no GPU device attached.

$ python3 benchmarking_suite.py --droplet debian-gpu-mi300x1-192gb-devcloud-atl1 \
    --run-id 2026-09-17-vllm-sweep-mi300x-rerun --repeat 3 --seed-base 27000
16 cells, 12 runnable, 4 infeasible at max_model_len 32768

The 32,768 row is infeasible rather than missing: 32,768 input plus 128 output does not fit a 32,768 context, and a cell that cannot exist is recorded as such rather than dropped.

One detail is load-bearing. vllm bench serve derives its prompts from --seed, which defaults to 0, and this deployment caches prefixes β€” so two runs sharing a seed measure the cache, not the card. Every cell and every repeat here uses a seed no other run in the sweep uses.

Output tokens per second, median of three repeats, worst-cell coefficient of variation 7.96%:

context 1 stream 4 16 64
128 340.9 1,124.1 3,569.1 πŸ₯‡ 10,284.0
1,024 305.1 969.1 2,653.6 5,398.2
8,192 192.8 445.5 688.2 702.7

Counting prefill as well, the busiest cell moves 48,583 tokens a second β€” 64 streams at 1,024 context. Single-stream latency at 1,024 context is 32.67 ms to first token and 3.04 ms per token after it.

Two things happen as context grows, and only one of them is the one people plan for.

Time per output token barely moves: 2.85 ms at 128 context, 3.04 at 1,024, 3.9 at 8,192. Time to first token rises steeply: 12.97 ms, 32.67 ms, 169.61 ms. Decode stays cheap; prefill gets expensive.

So the 8,192 row flattens β€” 688 tok/s at 16 streams, 703 at 64 β€” while the 128 row keeps climbing to 10,284. The constraint at long context is prefill, not memory. The heaviest cell in the grid wants 64 x 8,192 = 524,288 KV tokens against the 9,026,017 the engine allocated β€” 5.8% of the pool, by arithmetic.

That inverts the sizing rule the TPU rigs in this monorepo run on, where KV capacity is the thing you run out of first. On a 192 GB card serving a 2B model, it never becomes the binding constraint.

Throughput divided by hourly rate, in=1024, out=128. Every row is the same checkpoint under vLLM, each from its own schema-valid report:

rig $/hr basis 1 4 16 64
MI300X 1.99 on-demand 153 487 1,333 πŸ₯‡ 2,713
TPU v5e-1 0.5779 spot πŸ₯‡ 208 πŸ₯‡ 711 πŸ₯‡ 1,572 1,972
TPU v6e-1 2.97 on-demand 67 233 587 719
NVIDIA L4 0.94 spot 49 187 β€” β€”

Tokens per second per dollar-hour. The MI300X wins the busiest column outright and the v5e wins the other three β€” but those two rows are not priced the same way, which is the whole of the next section.

A spot rate against an on-demand rate is a discount, not a hardware result. Put all three on on-demand list and the ranking is uniform:

rig $/hr 1 4 16 64
πŸ₯‡ MI300X 1.99 153 487 1,333 2,713
πŸ₯ˆ TPU v5e-1 1.20 100 342 757 950
πŸ₯‰ TPU v6e-1 2.70 74 257 646 790

At matched pricing the MI300X returns 1.4x to 2.9x the tokens per dollar of a v5e β€” 1.53x, 1.42x, 1.76x and 2.86x across the four concurrencies, by arithmetic over the two measured rows above.

That advantage has one real qualifier: DigitalOcean publishes no preemptible tier for GPU droplets, so $1.99 is a floor. The v5e spot rate of $0.5779 per chip-hour is a purchasable option, and at 16 streams or fewer it buys more tokens per dollar than this card does.

So the honest form of the finding is conditional. If you want a non-preemptible box, or you keep dozens of streams busy, this is the best tokens-per-dollar measured across these rigs. If your traffic is light and you tolerate preemption, a spot TPU is cheaper.

Powering the droplet off does not stop DigitalOcean billing it. The resources stay reserved and the hourly rate keeps running; only destroying the droplet stops the meter.

That is why this toolkit ships no create and no destroy tool. Both are dollar-per-hour decisions, they stay a deliberate step in the console, and a rate quoted per hour means nothing until you know which hours you are paying for.

$ stop_vllm
βœ… `docker rm -f vllm` exited 0.

That frees the card's memory. It does not free your wallet.

The goal of this article was to deploy Gemma 4 E2B to a single AMD Instinct MI300X and measure what its hourly rate returns in tokens. The key to the solution was a tag-scoped MCP toolkit that reaches the card over SSH and the DigitalOcean API, because the machine running it has no GPU. The measured results were:

docker run to an endpoint that answers One droplet, one MI300X, region atl1, three repeats per cell with a unique prompt seed each, worst-cell coefficient of variation 7.96%. The comparison rows differ from this one in ways worth naming once: the TPU and L4 reports are single runs per cell on earlier vLLM builds, the v5e and L4 figures were measured on spot capacity while the MI300X and v6e were on-demand, and nothing else in this monorepo serves this checkpoint on AMD, so there is no twin to difference against.

The strategy for using MCP for AMD Instinct deployment and benchmarking was validated with an incremental step by step approach.

── more in #ai-infrastructure 4 stories Β· sorted by recency
── more on @amd 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/serving-gemma-4-on-a…] indexed:0 read:10min 2026-09-17 Β· β€”