This article is the v5e follow-on to the v6e-1 debugging guide. Same MCP tooling, same Antigravity CLI driver, smaller and cheaper silicon โ and a different set of failure modes. This time the model is google/gemma-4-E2B-it
on a single Cloud TPU v5e chip, with measured throughput, measured latency, and a cost breakdown that actually uses the numbers from the sweep.
Same brief as before: a DevOps/SRE assistant whose brain is a self-hosted Gemma 4 model. The MCP server provisions the TPU, deploys the vLLM container, discovers the endpoint, and then uses that endpoint to analyze Cloud Logging output. 31 tools, one server.py
, stdio transport.
What changed is the target: TPU v6e-1 โ TPU v5e-1, and Gemma 4 4B โ Gemma 4 2B.
The interesting question this article answers: v5e is roughly half the price of v6e per chip-hour. Is it half the machine, or worse?
| Spec (per chip) | TPU v5e (v5litepod ) |
TPU v6e (Trillium) | Ratio |
|---|---|---|---|
| HBM capacity | 16 GB | 32 GB | 2.0ร |
| HBM bandwidth | 800 GiBps | 1,638 GBps | ~2ร |
| Peak BF16 | 197 TFLOPs | 918 TFLOPs | 4.66ร |
| Peak INT8 | 393 TOPs | 1,836 TOPs | 4.67ร |
| Single-chip machine type | ct5lp-hightpu-1t |
ct6e-standard-1t |
โ |
| On-demand list | ~$1.20 / chip-hr | ~$2.70 / chip-hr | 2.25ร |
| Flex-start (see below) | ~$0.60 / chip-hr | ~$1.35 / chip-hr | 2.25ร |
Spec figures are Google's own, from the v5e and v6e documentation. Note that Google quotes v5e bandwidth in GiBps and v6e in GBps โ normalize the units and the real ratio is ~1.9ร, not a clean 2ร. I'm not going to pretend that's a meaningful difference at this altitude, but don't quote "2ร the bandwidth" as if it were exact.
That table is the whole story in miniature. v6e is 2.25ร the price for 2ร the memory and roughly 2ร the bandwidth but 4.7ร the raw FLOPS. For a 2B decode-bound workload โ which is bandwidth-bound, not FLOPS-bound โ v5e is priced almost exactly right. For prefill-heavy or long-context work, where you actually burn the matrix units, v6e's 4.7ร starts to matter.
The sweep below bears this out.
Antigravity CLI is the successor to Gemini CLI โ the terminal-driven, agent-assisted coding tool. Install instructions:
Getting Started with Antigravity CLI
Start it and authenticate against a Google Cloud project:
agy
The single biggest structural change from the v6e rig: deployment parameters live in exactly one place, tpu.env
, and everything reads from it.
GOOGLE_CLOUD_PROJECT=aisprint-491218
GOOGLE_CLOUD_REGION=us-west4
GOOGLE_CLOUD_ZONE=us-west4-a
MODEL_NAME=google/gemma-4-E2B-it
ACCELERATOR_TYPE=v5litepod-1
TENSOR_PARALLEL_SIZE=1
It is consumed by server.py
(via load_dotenv
), mcp-run.sh
(which is what the MCP config actually launches), the Makefile
(via -include
), and set_env.sh
. A real environment variable always wins over the file in all four โ load_dotenv
doesn't overwrite, the wrapper only exports what's unset, and the Makefile uses ?=
โ so GOOGLE_CLOUD_ZONE=europe-west4-a make status
still works as a one-off override.
Change the zone once. Not in five places. This sounds like housekeeping until you spend an afternoon debugging a deploy that was reading a stale zone out of mcp_config.json
.
The MCP config is correspondingly boring, which is the point:
{
"mcpServers": {
"tpu-2B-v5e1-devops-agent": {
"command": "/home/xbill/gemma4-queens/tpu-2B-v5e1-devops-agent/mcp-run.sh",
"args": [],
"env": {}
}
}
}
Compare that to the v6e-1 version, which inlined seven environment variables into the JSON. Every one of those was a place the config could drift.
(That absolute path is worth a second look if you're copying this: it points into a different checkout tree than the one this article was written from. Absolute paths in MCP configs are exactly the kind of thing that survives a directory rename and then fails silently at launch. mcp-run.sh
exists to keep everything else out of this file โ but the path to mcp-run.sh
itself is still a hardcoded string.)
v5litepod
Before anything else. gcloud does not know what a "v5e-1" is.
v5litepod-1
, v5e-1
v2-alpha-tpuv5-lite
ct5lp-hightpu-1t
, ct6e-standard-1t
"v5e-1" is fine in prose. It will never work in a gcloud argument. That last line matters more than it looks โ ct6e-standard-1t
is a v6e machine type, and if it shows up in your notes labelled as v5e, every memory number downstream of it is wrong by a factor of two. I found exactly that mislabel in this repo's own demo page while writing this article.
This is the one that cost the most time.
The MCP agent has a get_zones_with_available_quota
tool that scans TPUV5sLitepodPerProjectPerZoneForTPUAPI
across every zone:
> get_zones_with_available_quota
It came back with 44 zones with non-zero quota. Every single one. That number is useless, and here's why: quota only means creation is permitted. It says nothing about whether capacity exists, and โ critically โ nothing about whether the provisioning model you asked for is supported there.
Flex-start v5litepod-1
is a much narrower thing than the quota table suggests:
> create a v5litepod-1 queued resource in europe-west4-a
ERROR: FLEX_START provisioning model is not supported for accelerator type
"v5litepod-1" in location "europe-west4-a"
Same rejection in europe-west4-b
. Accepted in us-west4-a
. Verified by attempting creation in each โ there is no API that will tell you this in advance.
So the default zone this project shipped with (europe-west4-a
, inherited from the v6e rig) could never provision this rig, in any amount of retrying. Worth noting: the reference documentation lists europe-west4-b
as flex-start-capable for v5e, but its example uses v5litepod-4
. The single-chip shape is narrower than the table implies.
The agent keeps mutable state in tpu_zones_status.md
โ find_tpu
writes failures into it and reads it back to skip known-bad zones. At one point it looked like this:
| Zone | Quota Available | TPU v5e-1 Started Successfully | Details |
| europe-west4-a | Yes | No | creation failed |
| europe-west4-b | Yes | No | creation failed |
| europe-west1-b | Yes | No | creation failed |
| ... 40 more rows ...
Forty-odd zones "out of capacity" is not a capacity story. It's a flag story. Every one of those attempts was passing --network=vpc-glitnir
โ a VPC that does not exist in this project. aisprint-491218
has only the auto-mode default
network.
The fix was to let TPU_NETWORK
/ TPU_SUBNETWORK
default to empty so gcloud uses the project default, then reset the table and reseed it from a live quota scan. The lesson generalizes: if a resource fails in every zone, it isn't the zones.
Note also that the file is mutable state, not documentation. Don't hand-edit it as if it were docs โ find_tpu
will overwrite you.
With the zone sorted, the deploy is a single MCP call. The generated gcloud command:
gcloud alpha compute tpus queued-resources create vllm-gemma4-qr \
--node-id=vllm-gemma4-qr-node \
--project=aisprint-491218 \
--zone=us-west4-a \
--accelerator-type=v5litepod-1 \
--runtime-version=v2-alpha-tpuv5-lite \
--provisioning-model=flex-start \
--max-run-duration=4h \
--valid-until-duration=4h \
--labels=purpose=flex-start \
--metadata-from-file=startup-script=startup_script.sh
Those two duration flags are a local policy choice, not a platform limit. Flex-start itself allows up to seven days (maxRunDurationSeconds
, defaulting to the full seven). This rig caps at 4h deliberately: it's a demo box, and an auto-expiring TPU is cheaper than a remembered one. --valid-until-duration=4h
bounds the other end โ how long the request sits in WAITING_FOR_RESOURCES
before giving up rather than queuing indefinitely.
The startup script pulls vllm/vllm-tpu:nightly
and serves:
vllm serve google/gemma-4-E2B-it \
--max-model-len 16384 \
--tensor-parallel-size 1 \
--disable_chunked_mm_input \
--max_num_batched_tokens 4096 \
--limit-mm-per-prompt '{"image":4,"audio":1}' \
--enable-auto-tool-choice \
--tool-call-parser gemma4 \
--reasoning-parser gemma4
--tensor-parallel-size
is 1. v5e-1 is a single chip. If you see 4
anywhere in a v5e-1 config, it's copy-paste from a larger topology and the engine will fail to initialize.
Two structural details worth stealing:
The startup script fetches the HF token itself. The rendered script is uploaded as instance metadata, so a baked-in token would be readable from the instance by anyone with compute.instances.get
. Instead it reads hf-token
from Secret Manager at boot via the metadata server, retrying for 30 minutes so an IAM grant applied after creation still lands. Shell tracing is off across the entire token section.
Serving flags live in exactly one function. _vllm_serve_flags()
builds the vLLM arg list from MAX_MODEL_LEN
, MAX_NUM_BATCHED_TOKENS
, LIMIT_MM_PER_PROMPT
, and TENSOR_PARALLEL_SIZE
. The startup script template takes the same values as placeholders. Both deploy paths and the generated one-liner therefore cannot disagree.
That template is consumed by str.format()
, incidentally, which means any literal { or } you add to that bash file breaks the deploy at format time โ a shell brace expansion, a
${VAR}
, a JSON literal. Escape as {{
/ }}
. The --limit-mm-per-prompt
JSON above is exactly the trap.Here is where the 16 GB starts to bite, and it's not obvious from the parameter count.
Gemma 4 E2B is a "2B" model, but the resident weight footprint under vLLM is ~8.97 GiB, not the ~4.5 GiB you'd naively compute from 2B parameters at bfloat16. The multimodal towers are not free.
On a v5e chip that arithmetic goes:
Physical HBM 16 GB (~15.5 GiB)
vLLM utilization cap (0.9) ~13.9 GiB
Resident model weights ~8.97 GiB
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Available for KV cache ~5.0 GiB
Versus the same model on a v6e chip, which has ~19.8 GiB left for KV โ roughly 4ร the cache pool for a 2.25ร price.
At the observed ~18 KiB/token KV footprint (15 layers materialize a cache, bfloat16), ~5.0 GiB works out to roughly 290,000 tokens, or about 17ร concurrent requests at the configured 16,384-token context.
Caveat, stated plainly: the ~5.0 GiB and ~290K figures are arithmetic derived from the measured weight footprint and the published chip capacity, not read out of a v5e engine log. The 8.97 GiB weights and 18 KiB/token are measured. Treat the KV numbers as a good estimate, and confirm against your own vllm
init log before you size a fleet on them.
The 17ร concurrency ceiling is not theoretical. It's exactly what falls over in the sweep.
Run with vllm bench serve
inside the container on the TPU VM, 128 output tokens per request, 20 grid points. Zero failures across the whole grid โ worth noting, because the v6e-1 grid on the same model had 27 of 156 points fail or get skipped.
| Conc | Context | req/s | Output tok/s | Total tok/s | Mean TTFT | p99 TTFT | TPOT |
|---|---|---|---|---|---|---|---|
| 1 | 128 | 0.99 | 126.9 | 254 | 22.5 ms | 75 ms | 7.76 ms |
| 1 | 1,024 | 0.96 | 123.4 | 1,110 | 48.4 ms | 105 ms | 7.79 ms |
| 1 | 4,096 | 0.87 | 110.8 | 3,655 | 149.5 ms | 208 ms | 7.92 ms |
| 1 | 8,192 | 0.83 | 105.8 | 6,875 | 174.5 ms | 180 ms | 8.15 ms |
| 1 | 15,000 | 0.70 | 90.1 | 10,643 | 351.2 ms | 358 ms | 8.42 ms |
| 4 | 128 | 3.38 | 432.5 | 865 | 55.7 ms | 94 ms | 8.84 ms |
| 4 | 1,024 | 3.40 | 435.1 | 3,916 | 32.4 ms | 42 ms | 8.97 ms |
| 4 | 4,096 | 3.12 | 399.7 | 13,190 | 61.5 ms | 100 ms | 9.47 ms |
| 4 | 8,192 | 2.78 | 355.8 | 23,126 | 92.9 ms | 169 ms | 10.41 ms |
| 4 | 15,000 | 1.45 | 185.3 | 21,904 | 615.0 ms | 2,616 ms | 13.23 ms |
| 16 | 128 | 8.41 | 1,076.9 | 2,154 | 169.5 ms | 289 ms | 13.59 ms |
| 16 | 1,024 | 7.33 | 938.8 | 8,449 | 198.5 ms | 352 ms | 15.53 ms |
| 16 | 4,096 | 4.79 | 612.8 | 20,221 | 365.8 ms | 974 ms | 23.10 ms |
| 16 | 8,192 | 3.40 | 434.8 | 28,260 | 983.1 ms | 2,207 ms | 29.20 ms |
| 16 | 15,000 | 1.32 | 169.3 | 20,003 | 3,319 ms | 8,220 ms | 68.73 ms |
| 64 | 128 | 14.67 | |||||
| 1,877.7 | |||||||
| 3,755 | 341.7 ms | 398 ms | 31.52 ms | ||||
| 64 | 1,024 | 11.52 | 1,474.9 | 13,274 | 686.5 ms | 1,347 ms | 37.68 ms |
| 64 | 4,096 | 6.43 | 822.9 | 27,154 | 2,505 ms | 5,277 ms | 55.99 ms |
| 64 | 8,192 | 2.66 | 340.3 | 22,119 | 3,077 ms | 6,870 ms | 69.24 ms |
| 64 | 15,000 | 1.59 | 203.1 | 24,003 | 7,755 ms | 16,516 ms | 72.96 ms |
Peak decode: 1,877.7 output tok/s at 64 concurrent, short context.
Peak aggregate: 28,260 total tok/s at 16 concurrent, 8K context โ that's prefill throughput, and it's the number to quote if your workload is RAG-shaped.
Single-stream decode is 90โ127 tok/s and remarkably flat. TPOT moves from 7.76 ms to 8.42 ms as context goes from 128 to 15,000 tokens โ an 8% degradation across a 117ร context increase. Decode on a 2B model is bandwidth-bound, and attention over even 15K tokens barely registers against the weight streaming cost. This is the number that matters for an interactive agent: a single user gets a consistent ~120 tok/s regardless of how much context you stuff in.
Batching scales cleanly up to about 4K context, then stops. Speedup over single-stream, by context:
| Context | 4 conc | 16 conc | 64 conc |
|---|---|---|---|
| 128 | 3.41ร | 8.49ร | 14.80ร |
| 1,024 | 3.53ร | 7.61ร | 11.96ร |
| 4,096 | 3.61ร | 5.53ร | 7.43ร |
| 8,192 | 3.36ร | 4.11ร | 3.22ร |
| 15,000 | 2.06ร | 1.88ร | 2.26ร |
Look at the 8,192 row. Going from 16 concurrent to 64 concurrent makes throughput go down โ 434.8 tok/s to 340.3 tok/s. That is the ~5 GiB KV pool saturating. 64 requests ร 8,192 tokens is ~524K tokens of KV demand against a pool that holds ~290K. vLLM starts preempting and recomputing, and you pay for the same prefill twice.
The 15,000-token column is a wall, not a slope. At concurrency 1 it's fine โ 90 tok/s, 351 ms TTFT. At concurrency 16, p99 TTFT is 8.2 seconds. At 64, it's 16.5 seconds. Nothing failed, which is the good news; vLLM queued rather than OOM'd. But a 16-second p99 is a broken user experience.
The practical envelope for this rig:
| Workload | Recommended concurrency | What you get |
|---|---|---|
| Chat / short prompts (โค1K) | 64 | ~14.7 req/s, sub-700 ms TTFT |
| Tool-calling agent (โค4K) | 16 | ~4.8 req/s, ~366 ms TTFT, p99 under 1 s |
| RAG / long docs (8K) | 16 | 28,260 tok/s aggregate, ~1 s TTFT |
| Max context (15K) | 1โ4 | Anything above 4 has an unusable p99 |
Now the part the sweep pays for.
First, an honesty note about the flex-start column, because I got this wrong on my first pass. Google does not publish a per-chip-hour flex-start rate. Flex-start is billed under Dynamic Workload Scheduler pricing, described only as "discounted (up to 53%) for vCPUs, GPUs, and TPUs." So every flex number below is on-demand list minus an assumed 50%, which is the conservative end of "up to 53%". Treat them as upper bounds on the flex price, and therefore the cost-per-token figures as upper bounds too.
On-demand rates are list, us-west4:
| Configuration | Flex-start (derived) | On-demand (list) |
|---|---|---|
| TPU v5e-1 (16 GB HBM) | ~$0.60 / hr | $1.20 / hr |
| TPU v6e-1 (32 GB HBM) | ~$1.35 / hr | $2.70 / hr |
| TPU v6e-4 (128 GB HBM) | ~$5.40 / hr | ~$10.80 / hr |
| GCE 1ร NVIDIA L4 (24 GB) | ~$0.30 / hr (spot) | ~$1.00 / hr |
| GCE 8ร NVIDIA L4 (192 GB) | ~$2.40 / hr (spot) | ~$8.00 / hr |
| Cloud Run 1ร L4 (serverless) | โ | ~$1.20โ1.50 / hr active |
AWS EC2 g6.xlarge (1ร L4) |
||
| ~$0.35 / hr (spot) | ~$0.97 / hr | |
Azure NVadsA10v5 (1ร A10G) |
||
| ~$0.35 / hr (spot) | ~$1.05 / hr |
For reference, published spot v5e is around $0.35/chip-hr and spot v6e $0.60โ1.30/chip-hr โ cheaper than flex-start, but preemptible with 30 seconds' warning, which is a different risk profile than flex-start's bounded run duration.
Rates vary by region and change; check current pricing before you commit. One warning from this codebase: estimate_deployment_cost
in server.py:880
carries "v5e": 0.12
in its rate table, which is off by an order of magnitude against the $1.20 list rate. Don't trust a hardcoded rate table โ including this one.
Cost per million output tokens, computed directly from the measured sweep:
| Concurrency | Context | Output tok/s | Flex ($0.60/hr) | On-demand ($1.20/hr) |
|---|---|---|---|---|
| 1 | 128 | 126.9 | $1.314 / M | $2.627 / M |
| 4 | 128 | 432.5 | $0.385 / M | $0.771 / M |
| 16 | 128 | 1,076.9 | $0.155 / M | $0.310 / M |
| 64 | 128 | 1,877.7 | $0.089 / M | |
| $0.178 / M | ||||
| 16 | 1,024 | 938.8 | $0.178 / M | $0.355 / M |
| 64 | 1,024 | 1,474.9 | $0.113 / M | $0.226 / M |
| 16 | 4,096 | 612.8 | $0.272 / M | $0.544 / M |
| 64 | 4,096 | 822.9 | $0.203 / M | $0.405 / M |
The headline: $0.089 per million output tokens at peak batching on flex-start.
The more useful headline is the spread. The same chip, same model, same hour of billing, run at concurrency 1 instead of 64, costs 14.8ร more per token. Idle TPU is the entire cost story. An agent rig that sits at concurrency 1 is paying $1.31/M for tokens it could be getting at $0.09/M.
Which leads to the real cost question for a devops agent: your workload probably is concurrency 1. An SRE assistant answering one operator's question is a single stream, and no amount of tuning turns one operator into a batch of 64.
In that regime the cost control is run duration, not batching. This is why the rig sets --max-run-duration=4h
rather than accepting the seven-day flex-start default: an agent box that auto-expires costs a bounded amount, and a forgotten one costs $0.60/hr forever while serving nobody. Provision it for the session, let it expire. Do not leave a TPU idling at concurrency 1 and then compare $/M against a hosted API โ you will lose that comparison badly, and you will deserve to.
The counterweight, and the reason not to set the cap too aggressively: re-provisioning is not instant. Flex-start requests sit in WAITING_FOR_RESOURCES
until capacity appears, with no documented SLA on how long that takes. On this project it has run to a couple of hours. A 4-hour cap on a demo box is a reasonable trade; a 1-hour cap on something you actually need would not be.
Here I have to be careful, and I'd rather flag it than quietly paper over it.
The v5e-1 numbers above come from vllm bench serve
with a 128-token output per request. The v6e-1 Gemma 4 2B grid in this repo came from a different harness with a much shorter output window. Request-per-second figures between those two runs are not directly comparable โ a shorter output inflates req/s roughly linearly, and the v6e grid's peak of ~140 req/s is largely measuring prefill-and-stop, not sustained generation.
So the honest comparison is on axes where the measurement method doesn't dominate:
| Dimension | v5e-1 (measured) | v6e-1 | Verdict |
|---|---|---|---|
| Resident weights (E2B) | ~8.97 GiB | ~8.97 GiB | Model property, identical |
| HBM left for KV | |||
| ~5.0 GiB (derived) | |||
| ~19.8 GiB (measured) | v6e ~4ร the pool | ||
| KV capacity | ~290K tok (derived) | 1,151,744 tok (measured) | v6e ~4ร |
| Sustained decode, 1 stream | 90โ127 tok/s | ~214 tok/s | v6e ~1.7โ1.8ร |
| TTFT, 1 stream, short ctx | 22.5 ms (on-host) | ~210 ms (over network) | Not comparable โ different vantage points |
| Grid failures | 0 / 20 | ||
| 27 / 156 | v5e ran the whole grid | ||
| Max usable context @ conc 16 | ~8K | 65K configured | v6e, decisively |
| Price (flex) | ~$0.60/hr | ~$1.35/hr | v5e 2.25ร cheaper |
Single-stream decode: v6e is roughly 1.7โ1.8ร faster (~214 vs ~120 tok/s), for 2.25ร the price. On a per-dollar basis for a single interactive user, v5e is ahead. That tracks the hardware โ decode is bandwidth-bound, v6e has exactly 2ร the bandwidth, and it converts about 85% of that into real tokens.
Memory is where v6e earns its premium, not compute. The 4ร KV pool is the difference between a model that batches 8K context at 16 concurrent and one that starts preempting. If your workload is short-context chat, you are paying for headroom you never touch. If it's RAG over long documents at any real concurrency, v5e's ~5 GiB pool is the binding constraint and no amount of tuning fixes it.
The FLOPS gap barely shows up. v6e has 4.7ร the BF16 throughput and delivers ~1.8ร the decode tokens. That gap is the clearest possible signal that serving a 2B model is a memory-bandwidth problem, not a matrix-multiply problem. It also means the v6e-4 topology โ 4 chips, 128 GB โ is badly over-provisioned for a 2B model; the earlier v6e-4 sweep found the 2B model became bounded by host CPU dispatch overhead, with the TPU processing tokens faster than the host could schedule them.
| If your workload is... | Use | Because |
|---|---|---|
| Single-agent SRE assistant, one operator | v5e-1 flex | |
| Best tokens-per-dollar at concurrency 1; $0.60/hr | ||
| Chat serving, short context, high concurrency | v5e-1 flex | |
| 14.7 req/s at $0.089/M output tokens | ||
| Tool-calling agent, โค4K context, โค16 concurrent | v5e-1 flex | |
| Comfortably inside the KV envelope, p99 under 1 s | ||
| RAG over 8K+ documents at concurrency >16 | v6e-1 | |
| v5e's ~5 GiB KV pool preempts; throughput inverts | ||
| Long context (15K+) at any real concurrency | v6e-1 | |
| v5e p99 TTFT hits 8โ16 s | ||
| Anything 2B at 4-chip scale | Neither | |
| Host-bound; spend the money on a bigger model instead |
The summary in one line: for Gemma 4 2B, v5e-1 is the right default and v6e-1 is a memory upgrade, not a speed upgrade.
Once the queued resource goes ACTIVE, endpoint discovery is dynamic โ discover_vllm_url()
lists queued resources in the zone, takes the first ACTIVE
one, resolves its node and external IP, and builds http://{ip}:8000
. Never hardcode it; the IP changes every time the resource is recreated.
> verify_model_health
The health check passed successfully!
โข Status: PASSED โ
โข Response: "Hello! Yes, the model is working. I..."
โข Latency: 0.31 seconds
> query_queued_gemma4_with_stats what is a TPU?
### ๐ Performance Stats
โข Time to First Token (TTFT): 0.022s
โข Throughput: 126.9 tokens/s
โข Mean TPOT: 7.76 ms
One last trap: raw /v1/completions returns an empty completion on -it models.
make query
and benchmarking_suite.py
both use it, so an empty result there is expected, not a broken deploy. server.py
uses /v1/chat/completions
throughout. Keep new code on the chat endpoint; raw completions are only useful for prefill-only benchmarks.
> make tools
31 tools registered.
GemmaTools.md
and the get_help
tool both build their tool list from mcp.list_tools()
, so they cannot drift from the @mcp.tool()
decorators. Add a tool, run make tools
, the doc regenerates. Source of truth either way is grep -n "^@mcp.tool" server.py
.
One split worth preserving if you fork this: create_tpu_queued_resource
is non-destructive โ it touches only the id it was given, which is what makes find_tpu
's zone sweep safe to run. manage_queued_resource
is destructive โ it deletes every queued resource in the zone that isn't the named primary. Two verbs, two blast radii, no overlap.
And: don't destroy a queued resource unless you mean it. Teardown is not part of routine debugging, and there is no documented SLA on how long a flex-start request waits for capacity โ on this project it has run to a couple of hours.
A single TPU v5e chip serves Gemma 4 2B at 90โ127 tok/s single-stream and 1,878 tok/s batched, for $0.60/hr flex-start โ about $0.089 per million output tokens at peak batching, or $1.31/M if you run it at concurrency 1 like most agent workloads actually do.
Against v6e-1 at 2.25ร the price, v5e gives up roughly 1.8ร decode speed and 4ร KV cache pool. The decode gap is a bandwidth story and v5e wins on a per-dollar basis. The KV gap is real and unfixable, and it is the actual reason to reach for v6e: not because v6e is faster, but because 16 GB runs out.
The three things that cost the most time were all configuration, not capacity: flex-start v5litepod-1
is only accepted in us-west4-a
; a nonexistent VPC in the create flags looked exactly like a global capacity shortage; and ct6e-standard-1t
in a note labelled "v5e" quietly doubled every memory number downstream. Quota is not availability, availability is not the provisioning model, and a resource that fails in every zone is never the zones' fault.