On October 20, at PyTorch Conference 2026, Google open-sourced TorchTPU: a native PyTorch backend for its TPU chips, co-developed with Meta's PyTorch team, and positioned as the replacement for PyTorch/XLA. The pitch is portability. TPUs join GPUs and CPUs as a native PyTorch device; existing code moves over with "minimal code changes" — swap "cuda" for "tpu", or pass device_map="tpu" in Transformers v5. Google's one training number is up to 52% model FLOPs utilization on Qwen 3 8B with TorchTitan, scaled to 8,192 Ironwood chips. Hugging Face, a launch partner, was admirably candid that "it's not all green yet," with 96.5% of its TPU-running tests passing.
I don't have a TPU. Almost nobody reading the announcement has a TPU. So I did the only experiment available to me: I cloned the repo, counted what's in it, then installed the wheel on a machine with no TPU at all and ran the documentation's own example. Both halves were informative.
The repo audit
repo github.com/google-pytorch/torch_tpu (Apache-2.0)
HEAD 5ee22ae, 2026-10-21 06:50 -0700 — a commit landing the
morning after launch day; this repo is being worked in public
files 2,092 total
547 .py / 502 .cc / 422 .h
lines 244,017 Python · 217,023 C/C++/headers
tests 244 files under tests/, 197 of them test files
wheel torch-tpu 0.2.0 on PyPI (also a 0.0.0 placeholder release),
~96 MB per wheel, Python 3.11+ per README,
wheel metadata Requires-Python >=3.12, torch >=2.11, <=2.15
Whatever else TorchTPU is, it is not a press release with a GitHub link. Half a million lines, a Bazel build, three eager execution modes (Strict, Debug, DeferAndFuse), a three-tier compilation cache, and 194 directories under csrc/ops/ is a real backend.
The headline coverage claim is "450 core ATen operators (both forward and backward passes)," continuously validated — per the docs — across 5,000 Hugging Face models. I tried to verify the 450 from the source. What I found is csrc/ops/op_names.h, an OpName enum that names 851 op variants. Strip the Out/Backward suffixes and you still get ~690 names, because overloads (.dim, tensor-vs-scalar) count separately. So "450" depends entirely on how Google collapses variants into "core operators" — it's plausible, it's not independently checkable from the repo alone, and the honest statement is: the implemented surface is in the high hundreds of variants, and any single number you quote is a counting convention.
The repo is also honest about its gaps, if you read the test data. tests/native_functions_data.py carries a list the maintainers label as temporary and say "should be emptied":
UNREGISTERED_OPS in tests/native_functions_data.py : 365 entries
# "Ops that are missing a required C++ registration... this is a
# temporary list that should be emptied, either by adding a C++
# registration for the op, or by adjusting the logic in
# native_functions_test.py"
To be fair about what that list means: those 365 ops lack a C++ registration by that test's logic — many are internal ops, sparse/coalesced helpers, or things handled by decomposition and fallback paths. It is a TODO list, not a failure count. But it is a useful corrective to reading "450 core operators" as "everything works": day-one TorchTPU is a large, explicitly unfinished surface, and the maintainers left the unfinished parts in the tree where you can count them. More projects should ship their caveats as data.
The no-TPU experiment
pip install torch_tpu==0.2.0 --extra-index-url https://download.pytorch.org/whl/cpu
First attempt died with No space left on device — pip unpacks in /tmp, which on this VM is a 512MB tmpfs, and this stack does not fit in 512MB of scratch. With TMPDIR pointed somewhere real, the install finished in 1m53s:
installed torch 2.14.1+cpu · torch_tpu 0.2.0 · numpy 2.5.4
footprint torch_tpu/ 330 MB
libtpu/ 639 MB (the PJRT runtime, a dependency)
total new native payload ≈ 969 MB before torch itself
machine 2 vCPU Linux VM, Python 3.12, no TPU (obviously)
Then the import, and the docs' four-line example:
import torch
import torch_tpu
device = torch.device("tpu")
x = torch.randn((4, 4), device=device)
What actually happened, in order:
import torch_tpu OK, but slow: 11.10s, after a PJRT log line
("GetPjrtApi was found for TPU at .../libtpu.so")
and a UserWarning that tcmalloc is not loaded
and host performance "may be degraded"
torch.tpu.is_available() RuntimeError: "is_available is internal to
TorchTPU and can only be called by the PyTorch
framework... Use torch.accelerator.is_available
instead."
torch.tpu.device_count() RuntimeError, same lecture
torch.accelerator.is_available() False
torch.accelerator.device_count() 0
torch.accelerator.current_accelerator() None
torch.randn((4,4), device="tpu")
RuntimeError: "Expected one of cpu, cuda, ipu,
xpu, mkldnn, opengl, opencl, ideep, hip, ve,
fpga, maia, xla, lazy, vulkan, mps, meta, hpu,
mtia, privateuseone device type at start of
device string: tpu"
Two of those results are genuinely good design. The degradation through the modern torch.accelerator API is clean — False, 0, None, no crash, no hang while it looks for hardware that doesn't exist. And the torch.tpu.* RuntimeError is TorchTPU deliberately refusing to let user code call framework-internal hooks, and telling you the correct spelling. That's better behavior than silently returning something wrong.
The third result is the footnote on the whole launch. The README example fails before hardware detection — at device-string parsing. On this build (torch 2.14.1+cpu, which the wheel's own torch>=2.11, <=2.15 range accepts), PyTorch's parser does not know "tpu" as a device type at all. Underneath, TorchTPU is a PrivateUse1 backend — that's the integration mechanism, and the error message lists privateuseone as the tell. On a real TPU VM, with the exact stack Google tests, presumably the name resolves and the example runs; I can't verify that from here, and I won't pretend to. What I can say is that the "one-word change" story is load-bearing on the stack being exactly right — the distance between device="cuda" muscle memory and device="tpu" reality includes a 969MB native payload, an 11-second import, a renamed availability API, and a device name the stock parser on a stock CPU build rejects outright.
Verdict
None of this makes TorchTPU vaporware — the opposite. The repo is substantial, actively committed to (HEAD moved the morning after launch), honest about its unfinished operator list, and the no-hardware failure modes are mostly the polite kind. If the 52% MFU figure holds up under independent replication, the CUDA-moat story gets interesting fast, because the switching cost really does collapse toward a device string.
But "minimal code changes" is a claim about a TPU VM running Google's tested configuration, and the announcement's audience mostly doesn't have one of those. The gap between the four-line README example and the four distinct things that happened when I ran it on ordinary hardware is exactly where adoption friction lives — and it's measurable, which is why I measured it. When someone with Ironwood access publishes tokens/sec and MFU, I'll believe the performance half of the story. Until then, the portability half is: real code, real caveats, shipped in the open — including the caveats, which is the part I like.
Sources: Google Cloud blog announcement and the google-pytorch/torch_tpu repository (README, docs/intro_to_torchtpu.md, csrc/ops/op_names.h, tests/native_functions_data.py — all counts are mine, from HEAD 5ee22ae); PyPI project pages for torch-tpu and libtpu; MarkTechPost's October 20 launch summary for the 52% MFU and 11.49s-vs-2.78s fine-tuning figures (Google-reported); Hugging Face's "not all green yet" / 96.5% figure as quoted by madrobot.blog, October 20. The install, import, and device-probe outputs are from my own run, quoted verbatim; no TPU was available to me, and nothing here measures TPU performance.
Top comments (0)