Bazel Torch: From Empty Placeholder to Explicit Dependency (A PR Review)

2026/04/17

BUILDbazeltorchdependencies

TL;DR: Reviewing Yipu’s four-PR stack for explicit @system_torch targets. Route B (empty //bazel/torch placeholder) kept Bazel blind. Route C gives Bazel actual knowledge of torch’s location, version, headers, and shared libraries. GPU tests ⊂ torch tests ⊂ all tests, and the macro design reflects this asymmetry.

This is a PR review companion to the Three Ways to Smuggle torch In post. That one explored the options. This one covers a teammate’s PR stack that evolves Route C from “Bazel doesn’t know” to “Bazel actually tracks the dependency.”


The Review That Became Understanding

I was reviewing Yipu’s PR stack (DEVEX-962) and realized it answered a question I’d been sitting with since the original torch exploration: what’s the actual difference between “torch works in CI” and “Bazel knows torch exists”?

The answer is four PRs. Let me walk through what I learned.

The Venn Diagram You Need in Your Head

First, a thing that confused me during the review until I drew it out. We have three test macros in the monorepo:

The relationship is strict containment:

┌─────────────────────────────┐
│ All Tests (py_test)         │
│  ┌───────────────────────┐  │
│  │ Torch Tests            │  │
│  │  ┌─────────────────┐  │  │
│  │  │ GPU Tests        │  │  │
│  │  └─────────────────┘  │  │
│  └───────────────────────┘  │
└─────────────────────────────┘

Every GPU test needs torch. Not every torch test needs a GPU. The macro design reflects this: py_gpu_test auto-adds both "gpu" and "torch" tags, while py_torch_test only adds "torch".

This came up reviewing PR #30895, where @system_torch//:torch gets injected into BOTH macros. A natural question: “why inject torch into GPU tests if they already get it from… somewhere?” Because the tag is for filtering. The dependency injection is for the build graph. Different mechanisms, both necessary.

Three Routes, and Why Route B Was a Dead End

Quick recap from the smuggling post. The torch problem: uv.lock has macOS wheels only (Linux torch is Docker-preinstalled). Bazel on Linux can’t find @pypi//torch. 62% of packages blocked.

Three routes:

Route Mechanism Bazel’s Knowledge
A Add Linux wheels to uv.lock Full. But broken (marker interpretation disagrees with uv).
B (current main) uv.override_requirement//bazel/torch (empty py_library) Zero. Bazel sees a target with no files.
C (Yipu’s stack) uv.override_requirement@system_torch//:torch (repo rule) Real. Location, version, headers, .so files.

Route B is what we shipped in the override post. It works. Tests pass. But it’s a lie. Bazel thinks torch is an empty library with zero files. It can’t invalidate caches when torch updates. It can’t tell cmake that a C++ target depends on torch headers. It’s a hole in the dependency graph shaped exactly like our largest dependency.

Route C fills that hole.

The Four-PR Stack

Yipu’s stack, each PR building on the previous:

PR #30890: copy_so_to_package

A new Starlark rule that symlinks cmake .so outputs into a py_library. This is plumbing. cmake foreign rules produce shared libraries in Bazel’s output tree, but Python code expects them in the package directory. This rule bridges the gap.

PR #30892: system_torch Repository Rule

The core of the stack. A repository rule that runs at workspace setup time, discovers the system torch installation, and creates two targets:

This is the key architectural move. One discovery mechanism, two targets for two languages. Bazel now has real information about torch instead of a blank placeholder.

PR #30895: Macro Injection

Injects @system_torch//:torch as a dependency into both py_gpu_test and py_torch_test macros. Every test that declares itself as needing torch now gets the real, discovered torch in its dependency graph. No more include_system_site_packages=True grabbing the entire system.

PR #30896: cmake() Gets torch_cpp

The payoff for having a C++ target. cmake(mai_kernels_cuda) can now declare deps = ["@system_torch//:torch_cpp"]. Bazel tracks the dependency. Cache invalidation works. The cmake foreign rule post described how cmake targets are opaque to Bazel. This makes one less thing opaque.

Before and After

BEFORE (Route B, current main):
  @pypi//torch → //bazel/torch           (empty target, dead end)
  cmake(mai_kernels_cuda) → find_package  (Bazel doesn't know about torch)
  py_gpu_test → system site-packages      (grabs everything, no isolation)

AFTER (Route C, this stack):
  @pypi//torch → @system_torch//:torch    (repo rule, real discovery)
  cmake(mai_kernels_cuda) → @system_torch//:torch_cpp  (Bazel tracks it)
  py_gpu_test → @system_torch//:torch     (explicit dep in the graph)

The before/after for Python tests is functionally identical today. Tests pass either way. The difference is structural: Bazel’s dependency graph now reflects reality instead of containing a lie.

For C++ targets, the difference is immediate. Bazel can now cache-invalidate cmake builds when torch changes. That’s not theoretical. We update torch versions in the Docker image, and before this stack, cmake targets had no idea.

What I Took Away

PR review is underrated as a learning mechanism. I’d been writing about the torch problem for weeks (the resolution pipeline, the three routes, the system torch implementation). But reviewing someone else’s solution to the same problem made me articulate things I’d only vaguely understood. The Venn diagram. The difference between “it works” and “Bazel knows.” The gap between Route B and Route C.

Sometimes you don’t understand a problem until you review someone else’s answer to it.