Bazel Torch: Presenting the PR to the Team

2026/03/03

BUILDbazelcommunicationtorch🍞

What this is: A rehearsal guide for presenting PR #22703 (Bazel torch migration proof-of-concept) to the DevEx squad and broader team. Not a technical reference β€” that’s the deep-dive post. This post is about what to say, how to say it, and what questions to expect.

After reading this, you should be able to:


The Elevator Pitches

30 seconds β€” standup / async update

“PR 22703 is ready for review. It migrates item_response_theory to Bazel β€” the first torch-dependent package. The key thing: it establishes the pattern for ALL torch packages going forward. One new macro called py_torch_test, Docker-based CI job, both CI checks green. After this merges, migrating the next torch package is a checklist, not a project.”

What to emphasize: “first torch package” + “establishes the pattern” + “CI green” What to skip: Everything about how it works internally

2 minutes β€” sprint review / broader sync

“So we had this blocker in the Bazel migration β€” torch. Can’t add it as a normal pip dependency because it’s 2GB, needs CUDA, and has platform-specific wheels. The solution is a two-tier architecture.

Tier one: normal tests run hermetically, same as before. No Docker, fast.

Tier two: torch tests run inside a Docker container where torch is pre-installed as a system package. We use a Bazel rule called py_venv_test that lets Python see system packages β€” so torch is visible at runtime even though Bazel doesn’t manage it directly.

The PR adds one new macro β€” py_torch_test β€” that wraps all of this. CI routes tests to the right job using tags. item_response_theory is the proof-of-concept. Both CI jobs passed first try.

The important part: this pattern is now reusable. Next torch package is just a checklist β€” add a BUILD file, tag the tests, push.”

What to emphasize: The problem (torch can’t be a pip dep), the two tiers, the macro, and the reusability What to skip: MODULE.bazel toolchain details, antlr4 override, gazelle directives

5 minutes β€” technical deep-dive with the squad

Use the 2-minute version as a base, then add:

“Let me walk through how it actually works.

The core trick is the -I flag(Pythonηš„ιš”η¦»ζ¨‘εΌοΌ‰. Normal py_test passes -I, which blocks system packages. py_venv_test strips that flag and creates a venv that inherits system site-packages. So when your test does import torch, Python finds it in Docker’s /usr/local/lib/python3.12/site-packages/.

For Bazel to use Docker’s Python instead of downloading its own, we register a local toolchain in MODULE.bazel. It points at /usr/local/bin/python3, with on_failure = skip so macOS just falls through to the hermetic interpreter.

The Gazelle side is a bit counterintuitive. We add # gazelle:ignore torch to the import line β€” but that doesn’t mean we’re ignoring torch. It means we’re telling the BUILD file generator to skip it. Torch still works from the system. It’s a build-time annotation, not a runtime one.

CI runs two parallel jobs against the same test universe. One excludes torch-tagged tests, the other includes only torch-tagged tests. Every test ends up in exactly one job.

I picked item_response_theory because it’s the simplest torch-dependent package β€” two source files, one test. It proved the full pattern with zero noise.”

What to emphasize: The -I flag story, the two CI jobs, why this specific package was chosen What to skip: antlr4/sdist details (unless asked), pytest_runner internals, Label() vs string paths


Slack Message Templates

PR ready for review

πŸ“¦ PR ready: #22703 β€” Migrate item_response_theory to Bazel (torch pattern proof-of-concept)

This is the first torch-dependent package migrated to Bazel. It establishes the reusable pattern for all torch packages:

Both CI jobs green βœ… 13 files, 172 insertions, 1 commit

The pattern is documented in the macro and BUILD files β€” migrating the next package is a checklist.

Would appreciate a review when you get a chance πŸ™

Reply to “what does gazelle:ignore do?”

gazelle:ignore torch tells Gazelle (the BUILD file generator) to not add @pypi//torch to the deps list. It’s a build-time annotation only.

Torch still works at runtime β€” it comes from Docker’s system Python. Without the annotation, Gazelle would try to add torch as a pip dependency, which is the exact thing we’re avoiding (2GB download, CUDA, etc).

Think of it as: “hey Gazelle, I know this import exists, and I know you can’t find it in @pypi//. Trust me, it’ll be there at runtime.”

Reply to “why not just add torch as a pip dep?”

Three reasons torch can’t be a normal @pypi// dependency:

  1. Size β€” ~2GB wheel. Downloading per-build is a non-starter
  2. Platform β€” CPU vs CUDA wheels, different CUDA versions, Linux vs macOS. The pip resolution gets really messy
  3. sdist deps β€” torch’s ecosystem has packages distributed as sdist-only (no pre-built wheels). aspect_rules_py can only install wheels, can’t build from source

So torch lives in Docker as a system package. Deterministic environment, controlled CUDA version, no per-build download.


Anticipated Questions β€” Rehearsal Scripts

Read through these as if you’re in a meeting. Practice saying your answers out loud. Focus on the branching responses β€” those are the high-value rehearsals.


“Why can’t torch be a normal pip dependency?”

Teammate asks something like: “Can we just pip install torch in Bazel like we do with numpy?”

Your answer:

“Not really β€” three problems. Torch is 2GB, so downloading it every build is too slow. It’s platform-specific β€” you need different wheels for CPU vs CUDA, different CUDA versions. And some of its transitive deps are sdist-only, which our Bazel Python rules can’t build. So instead, we put torch in the Docker image and let the test see it as a system package.”

(if they push back: “but can’t we cache the download?”)

“Even cached, the resolution is messy. Different CI runners might need different variants β€” CPU for unit tests, CUDA 12.4 for GPU tests. And the sdist problem doesn’t go away with caching. Docker gives us a clean boundary: the image IS the version pin.”


“What does gazelle:ignore actually do?”

Teammate asks: “I see gazelle:ignore torch in the code β€” does that mean torch is disabled?”

Your answer:

“No, the opposite β€” torch still works fine at runtime. gazelle:ignore is a note to the BUILD file generator, not to Python. It says: don’t add @pypi//torch to the dependency list. Because torch comes from the Docker image’s system Python, not from Bazel’s pip mechanism.”

(if they look confused:)

“Think of it like thisοΌˆζ‰“δΈͺζ―”ζ–ΉοΌ‰β€” if Bazel’s package manager is a delivery service, gazelle:ignore says ‘don’t order torch for delivery, it’s already in the fridge.’ The import still works, it just comes from a different place.”


“Why py_venv_test instead of py_test?”

Teammate asks: “Why the different test rule? What’s wrong with py_test?”

Your answer:

py_test always runs Python in isolated mode β€” the -I flag. That means Python only sees packages Bazel manages. It can’t see system packages at all. Which is great for hermeticity, but it means Python can’t see Docker’s torch.

py_venv_test has a flag called include_system_site_packages that strips the -I flag. So Python creates a venv that inherits system packages. That’s how torch becomes visible.”

(if they ask “what’s the -I flag?”):

“It’s Python’s isolated modeοΌˆιš”η¦»ζ¨‘εΌοΌ‰. When you start Python with -I, it ignores everything on the system β€” PYTHONPATH, site-packages, all of it. Only sees what you explicitly pass. Bazel uses it for reproducibility, but it blocks us from seeing system torch.”


“Is py_venv_test a private API? Is that risky?”

Teammate asks: “I noticed the bzl-visibility disable comment. Is this going to break?”

Your answer:

“Yes, it’s a private API β€” we’re aware of the tradeoff. The public py_test has no way to disable isolated mode. There’s no flag for it. So we use the private rule.

If aspect_rules_py refactors their internals, our import path might break. But the fix would be a single line change in one file β€” defs.bzl. We marked it with the disable comment so it’s explicit and easy to find.”

(if they ask “should we contribute upstream?”):

“That’s a good idea for later. For now, the private API gets us unblocked. An upstream contribution would mean waiting for a release cycle, and we have 30+ packages waiting to migrate.”


“What happens on macOS?”

Teammate asks: “I develop on a Mac. Will this break my local bazel test?”

Your answer:

“Nope. Two safety nets. First, the torch tests have target_compatible_with = Linux only, so on macOS they’re silently skipped β€” not failed, just skipped. Second, the local toolchain in MODULE.bazel has on_failure = skip, so if /usr/local/bin/python3 doesn’t have torch, Bazel falls back to its hermetic interpreter. You don’t need to do anything.”


“How do I migrate MY torch package next?”

Teammate asks: “I have a package that depends on torch. What do I do?”

Your answer:

“Five steps:

  1. Add # gazelle:ignore torch to any file that imports torch
  2. Write BUILD.bazel files β€” use py_torch_test for the tests. Copy item_response_theory as a template
  3. Add your directory to GAZELLE_DIRS in bazel/BUILD.bazel
  4. If your package needs extra system deps β€” like transformers or flash-attn β€” add them to Dockerfile.torch-test
  5. Push and see if CI passes

The macro, the runner, the toolchain, the CI pipeline β€” all already in place. You’re just plugging in.”


“Why this package first?”

Teammate asks: “Why item_response_theory and not something bigger?”

Your answer:

“It’s the simplest torch-dependent package. Two source files, one test file. Direct torch usage β€” no transitive torch deps through hydra or datadog. Zero extra Docker deps needed. Tests passed first try in CI. The goal was to prove the pattern with minimum noise, so that the review focuses on the architecture, not on a hundred package-specific issues.”


“Isn’t this over-engineered? One macro, a runner, a Dockerfile…”

Teammate asks: “This seems like a lot of machinery for one test.”

Your answer:

“It’s not for one test β€” it’s for ALL torch tests. Right now there’s 30+ packages waiting on this pattern. The macro is 20 lines. The pytest runner is 26 lines. The Dockerfile was already there, just needed two more pip install lines.

Without the macro, every torch test BUILD file would need about 15 lines of boilerplate instead of 5. The runner exists because py_venv_test doesn’t support pytest_main. Each piece has a specific reason.”

(if the concern is specifically about the macro count):

“Just to be clear β€” we only added ONE new macro: py_torch_test. The py_test macro already existed before this PR. The pytest runner is a script, not a macro.”


Quick Concept Refreshers

If someone uses a term and you need a quick mental recall, scan this section.

Hermetic vs non-hermetic Python in Bazel

py_test vs py_venv_test

Toolchain registration

Tag-based CI routing

gazelle:ignore vs gazelle:resolve vs gazelle:exclude

Directive What it does When to use
gazelle:ignore “Don’t add this import to deps” Torch β€” it comes from system, not @pypi//
gazelle:resolve “This import maps to THIS Bazel target” Non-standard layouts where Gazelle guesses wrong
gazelle:exclude “Don’t auto-generate targets for these files” Test files that use a custom macro instead of py_test

Mental Models

The bouncer analogy β€” the -I flag

The -I flag is a bouncer at the door of Python’s runtime. It blocks system packages from entering. Normal Bazel tests have this bouncer.

py_venv_test with include_system_site_packages=True fires the bouncer. System packages can walk right in. But we only fire the bouncer inside Docker, where we control exactly who’s in the building.

The two-lane highway β€” CI routing

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                CI Trigger                β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Hermetic Lane   β”‚    Docker Lane       β”‚
β”‚  (fast, no Docker)β”‚   (torch, Docker)   β”‚
β”‚                  β”‚                      β”‚
β”‚  py_test         β”‚  py_torch_test       β”‚
β”‚  -I flag ON      β”‚  -I flag OFF         β”‚
β”‚  @pypi// only    β”‚  @pypi// + system    β”‚
β”‚  tags: -torch    β”‚  tags: torch         β”‚
β”‚                  β”‚                      β”‚
β”‚  numpy βœ“         β”‚  numpy βœ“             β”‚
β”‚  scipy βœ“         β”‚  scipy βœ“             β”‚
β”‚  torch βœ—         β”‚  torch βœ“ (from Docker)β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Two lanes, same highway, same codebase. Tag-based routing. Every test is in exactly one lane.

The “delivery vs fridge” model β€” gazelle:ignore

@pypi// is a delivery service. Bazel orders packages and they show up.

gazelle:ignore torch says: “don’t order torch for delivery. It’s already in the fridge (Docker’s system Python).”

The import import torch still works β€” Python checks the fridge (system site-packages). Gazelle just doesn’t need to put it on the shopping list (BUILD deps).


What NOT to Say

These are common phrasings that will confuse teammates. Avoid them.

❌ Don’t say βœ… Say instead Why
“We ignored torch” “We told the BUILD generator to skip torch. Torch still works from Docker.” “Ignored” sounds like torch is disabled
“We use a private API” (without context) “We use py_venv_test, which is internal to aspect_rules_py. Conscious tradeoff β€” no public alternative for this.” Without context, “private API” sounds reckless
“The -I flag” (to non-Bazel people) “Python’s isolation mode that blocks system packages” Nobody outside Bazel-land knows what -I is
“Hermetic” (to non-build-system people) “Isolated” or “sandboxed” “Hermetic” is jargon
Deep-dive into antlr4/sdist Only if specifically asked It’s a workaround for a transitive dep. Confusing side quest.
Explain MODULE.bazel toolchain registration proactively Only if someone asks “how does Bazel find system Python” Most people don’t care how the interpreter is selected
“There are multiple macros” “There’s ONE new macro: py_torch_test. The rest was already there.” The “too many abstractions” concern is easy to trigger

Cheat Sheet β€” The Whole PR in One Table

Print this. Glance at it before the meeting.

Aspect One-liner
What Migrate item_response_theory to Bazel β€” first torch-dependent package
Why Proves the reusable pattern for ALL torch packages
Core trick py_venv_test strips -I flag β†’ Python sees system torch
New macro py_torch_test β€” wraps py_venv_test + auto-tags torch
CI routing Two parallel jobs, tag-filtered: hermetic (-torch) and Docker (torch)
macOS Silently skipped β€” target_compatible_with = Linux, on_failure = skip
Package choice Simplest torch package. 2 files, 1 test, zero noise.
Unlocks 30+ torch packages can now follow the same checklist
Stats 13 files, 172 insertions, both CI jobs green βœ…
Risk py_venv_test is private API β€” 1 line to fix if it breaks