Skip to content

Add --mock_deps: build docs without installing heavy dependencies - #801

Merged
mishig25 merged 5 commits into
mainfrom
mock-imports
Jul 6, 2026
Merged

Add --mock_deps: build docs without installing heavy dependencies#801
mishig25 merged 5 commits into
mainfrom
mock-imports

Conversation

@mishig25

@mishig25 mishig25 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

What

doc-builder imports the documented library to introspect docstrings with inspect — necessary because libraries like transformers construct docstrings dynamically (decorators, templating), which rules out static analysis. The cost: building docs requires installing the library's full dependency tree — torch, GPU wheels, and in transformers' case a dedicated container image.

This adds --mock_deps torch,deepspeed,...: the listed packages are mocked at import time, so only the documented library itself (plus its light dependencies) needs to be installed.

How it works

A sys.meta_path finder (Sphinx's autodoc_mock_imports idea, extended for HF-library realities) provides for each mocked package:

  • Availability-check compatibility: HF libraries gate their exports on importlib.util.find_spec(pkg) + importlib.metadata.metadata(pkg) and route to dummy objects when they fail — naive sys.modules mocks don't pass these. The finder serves both module specs and fake distribution metadata.
  • Signature-safe subclassing: mocks used as base classes go through PEP 560 __mro_entries__, substituting a plain-type base — so a real class Model(nn.Module) keeps its real __init__ signature and docstring under inspect (a metaclass-based mock silently degrades every subclass signature to *args, **kwargs).
  • Decorator pass-through: calling a mock with a single function/class returns it unchanged, so @torch.no_grad()-style decorations don't swallow docstrings.
  • Faithful rendering: dotted-path repr (repr(torch.float32) == "torch.float32") and short __name__/__qualname__, so mocked defaults and annotations render like the real objects.

Packages that are actually installed are never mocked. doc-builder preview supports the flag too.

Fidelity verification

Built accelerate docs in a venv without torch or deepspeed installed and compared all 48 generated mdx pages against a real-torch reference build:

  • 46/48 byte-identical (modulo the pre-existing nondeterministic <object object at 0x…> addresses).
  • The remaining 2 pages differ only in typing-construct paths, where the mock renders the public import path (torch.nn.Module, torch.Generator) instead of the internal one the real class exposes (torch.nn.modules.module.Module, torch._C.Generator) — arguably an improvement.

Bonus fix while aligning rendering: get_type_name now prefers __qualname__ for non-typing objects, so builtin-function annotations render as callable instead of <built-in function callable> (which also removes a source of nondeterministic 0x… addresses in real builds).

Adoption path

No workflow changes required: repos can pass --mock_deps ... today via the shared workflows' existing additional_args input. For transformers this opens the door to dropping the GPU container for doc builds (pip install transformers --no-deps + light deps + --mock_deps torch,tensorflow,flax,...) — that migration should be validated per-library with the same mdx-diff methodology used here.

Also includes tests/test_mock_imports.py covering availability checks, subclass signature preservation, decorator pass-through, and rendering.

🤖 Generated with Claude Code

mishig25 and others added 2 commits July 5, 2026 21:24
doc-builder must import the documented library (docstrings are built
dynamically, e.g. transformers' add_start_docstrings decorators), which
historically forced installing the full dependency tree - torch, GPU
wheels, custom containers - just to build documentation.

--mock_deps torch,... installs a sys.meta_path finder providing, for
each mocked package and its submodules:
- importable module trees that pass the find_spec +
  importlib.metadata availability checks HF libraries gate exports on
  (naive sys.modules mocks fail these and route to dummy objects)
- mocks usable as base classes via PEP 560 __mro_entries__, which
  substitutes a plain-`type` base so real subclasses keep their real
  __init__ signature and docstring under inspect
- pass-through decorator behavior (calling a mock with a single
  function/class returns it unchanged), so decorated docstrings survive
- dotted-path repr and short __name__/__qualname__, so mocked defaults
  and annotations render like the real objects

Also: get_type_name now prefers __qualname__ for non-typing objects,
which renders mocked annotations like real classes and fixes real
builds rendering builtin-function annotations as
`<built-in function callable>` (now `callable`).

Fidelity, verified by building accelerate docs in a venv WITHOUT torch
or deepspeed installed (--mock_deps torch,deepspeed) against a real-
torch reference build: 46/48 pages byte-identical (modulo the known
nondeterministic 0x addresses); the remaining 2 differ only in typing
paths rendered from the public import path (torch.nn.Module) instead of
the internal one (torch.nn.modules.module.Module).

The documented library itself stays really installed (inspect reads its
real source); packages that are actually installed are never mocked.
`doc-builder preview` supports the flag too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Validated by building all 723 transformers en doc pages with
--mock_deps torch,tensorflow,flax,jax,torchvision,torchaudio,timm and
only light real deps installed (tokenizers, sentencepiece, ...): zero
conversion errors, full en mdx build in ~9s. Spot-check against
production pages built with real deps: bert.md and llama.md are
byte-identical (similarity 1.0000); trainer.md at 0.9984 with residual
diffs attributable to dependency version skew (older local accelerate).

Gaps found and fixed by the validation:
- PEP 604 unions in runtime-evaluated annotations (`int | torch.Tensor`)
  now build *real* typing.Union objects (mocks pass typing._type_check
  because they are callable), so tools unwrapping Optional/Union via
  typing.get_origin/get_args — like transformers' auto_docstring —
  treat them exactly like real annotations.
- mock modules are a ModuleType subclass carrying the same operator
  surface: `from torch import Tensor` on a mocked package materializes
  `torch.Tensor` as a module via _handle_fromlist, which then appears in
  unions/calls/base classes.
- mock instances expose `__module__` (the mocked path) so
  `__module__.__qualname__` renderers show `torch.Tensor`, not
  doc_builder.mock_imports.
- mock base classes gained class-level attribute fallback via a
  metaclass (real subclasses may rely on inherited attributes like
  nn.Module.forward) deriving from ABCMeta to stay combinable with real
  ABC bases; it defines no __call__, keeping subclass signatures intact.
- find_object_in_package: when a submodule is importable but not bound
  as an attribute of its (lazy) parent, resolve it from sys.modules and
  heal the binding — fixes transformers' fusion_mapping-style pages,
  independent of mocking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mishig25

mishig25 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a validation round against the full transformers docs, per the plan in the PR description:

Setup: venv with only light real deps (transformers --no-deps + tokenizers/sentencepiece/etc.), no torch/tensorflow/flax/jax/torchvision/torchaudio/timm installed, all of them mocked.

Result: all 723 en pages convert with zero errors; the full en mdx build takes ~9 seconds on a laptop (vs minutes in the GPU container). Fidelity spot-check against production pages (built with real deps, fetched from the doc bucket): model_doc/bert.md and model_doc/llama.md are byte-identical (similarity 1.0000); main_classes/trainer.md is 0.9984 with the residual diffs attributable to dependency version skew in my local env (older accelerate without ParallelismConfig), not mocking.

The validation surfaced and fixed five gaps (see commit message): real typing.Union construction for PEP 604 annotations, module-typed mocks from from torch import X imports, __module__ exposure for auto_docstring-style renderers, class-level attribute fallback on mock bases (ABCMeta-compatible), and a lazy-module attribute-binding bug in find_object_in_package that turns out to be mock-independent.

Each library gets a src/doc_builder/mock_deps/<library>.txt listing the
heavy dependencies to mock when building its docs; build/preview apply it
automatically, so workflows no longer need --mock_deps (kept as an
additive extra). Registry validated by building the docs of 17 HF
libraries end to end with only light real deps installed.

Also hardens the mocks for cases those builds surfaced: mocked module
__version__ compares against tuples like torch's TorchVersion, len() of
a mock returns 0, and class-attribute fallback on mocked bases is
restricted to a whitelist (forward/call) so hasattr probes on subclasses
stay accurate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mishig25

mishig25 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Added a per-library mock-deps registry (src/doc_builder/mock_deps/<library>.txt), applied automatically by build/preview — no --mock_deps flag needed anymore (kept as an additive extra for unregistered libraries).

Validated by building the docs of every doc-builder-based library end to end locally, installing only the library itself (--no-deps) plus light pure-Python deps:

library pages built mocked deps
transformers 723 torch, tensorflow, flax, jax, torchvision, torchaudio, timm, deepspeed
diffusers 363 torch
lerobot 97 (none — py3.12, light deps only)
timm 77 torch, torchvision
peft 76 torch
trl 66 torch
openenv 66 tomli_w, fastapi, fastmcp, mcp, openai, gradio, websockets
datasets 61 (none needed — pyarrow must stay real)
huggingface_hub 52 torch
accelerate 48 torch, deepspeed, torchvision
kernels 35 (none)
lighteval 27 torch, vllm, matplotlib, ray, more_itertools
bitsandbytes 25 torch
evaluate 20 torch, matplotlib
trackio 20 torch, matplotlib
autotrain 19 torch, loguru
tokenizers 18 (none — PyPI wheel)
safetensors 10 paddle, torch, tensorflow, jax

Fidelity checks: transformers bert/llama pages byte-identical to production; accelerate 46/48 mdx byte-identical to a real-torch build (the 2 diffs are torch-derived default values in signatures, known and acceptable).

Only exception: setfit (maintenance mode) pins old transformers/huggingface_hub versions whose real deps conflict — its registry file is included best-effort, but its docs still need a real install.

The validation surfaced three mock hardening fixes, all included: tuple-comparable __version__ (bitsandbytes does torch.__version__ >= (2, 6)), len() of a mock → 0, and class-attribute fallback on mocked bases restricted to a forward/call whitelist (a blanket fallback broke peft's prefix validation via hasattr).

Registry keys are the doc-builder library names, so namespace subpackages
get dotted files (optimum.intel.txt, optimum.neuron.txt, ...) and
optimum-onnx shares optimum.txt since its docs build under the library
name "optimum". MockFinder now matches registered names by prefix instead
of top-level root, so a registered name can itself be a dotted submodule
(mocking optimum.onnxruntime while the real optimum stays importable).

Validated end to end: optimum (16 pages), optimum-onnx (23),
optimum.intel (9), optimum.executorch (5), optimum.neuron (53, with the
workflow's notebook-to-mdx pre-steps), optimum.tpu (19), optimum.habana
(21, needs its version-pinned real deps plus a habana-torch-plugin
distribution visible to `pip list` because of an import-time subprocess
version check).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mishig25

mishig25 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Extended the registry to the optimum family — the special composite case where /docs/optimum is one site fed by many repos.

How that family builds today, for context:

  • The main optimum workflow builds its own docs, then docs/combine_docs.py inserts TOC sections for the subpackages. For every current subpackage (executorch/habana/intel/neuron/onnx/tpu) those are just links into each standalone /docs/optimum-xyz site via _redirects.yml — the legacy path that physically copied subpackage HTML into optimum's build is now dead code (nvidia/furiosa are skipped, everything else is "external").
  • Each optimum-xyz repo builds and pushes its own docs, but under the doc-builder library name optimum.xyz (dotted namespace module) with --repo_name optimum-xyz — and optimum-onnx builds under the library name optimum outright.

Registry consequences, all included here:

  • Registry keys are library names, so the new files are optimum.txt, optimum.intel.txt, optimum.executorch.txt, optimum.neuron.txt, optimum.tpu.txt, optimum.habana.txt; optimum-onnx shares optimum.txt.
  • MockFinder now matches registered names by prefix instead of top-level root, so a registered name can be a dotted submodule: optimum.onnxruntime can be mocked while the real optimum + optimum.intel stay importable (needed because subpackages of a namespace package can be individually missing).

Validation (only the library installed --no-deps + light real deps):

build pages mocked deps
optimum 16 torch
optimum-onnx (as lib optimum) 23 onnxruntime, onnx
optimum.intel 9 torch, openvino
optimum.executorch 5 (none)
optimum.neuron 53 torch, torch_xla, torch_neuronx, neuronx_distributed, libneuronxla, neuronxcc, peft
optimum.tpu 19 (none — no libtpu wheel index needed)
optimum.habana 21 torch

Caveats found along the way:

  • optimum-neuron's workflow runs doc-builder notebook-to-mdx pre-steps for ~10 tutorial pages; those must run before the build (reproduced locally).
  • optimum-habana pins exact real deps (transformers 4.55.x, accelerate ≤1.10.1, diffusers 0.35.1, sentence-transformers 3.3.1) and runs an import-time pip list | grep habana-torch-plugin subprocess version check, so a light build additionally needs a habana-torch-plugin distribution visible to pip list (a stub dist-info suffices). Noted in its registry file header.
  • The optimum family needs transformers 4.x (transformers@main lazy attrs break optimum.intel/optimum.executorch/optimum.neuron imports today).

With this, every library from moon-landing's docs index that documents a Python package validates against the registry — 24/25 builds green (setfit remains the lone blocked case).

The mock-deps registry now records both sides of a light doc build:
bare lines are mocked, real:<spec> lines are installed for real, and
nodeps:<spec> lines are installed with --no-deps (their own dependency
trees pull in heavy packages). The new `doc-builder light-install
<library>` command installs the real deps via uv, pinning bare names to
the documented library's own declared version ranges so registry files
never chase upstream pins (e.g. transformers' tokenizers range);
`--check` reports whether a registry entry exists (exit 3 when not).

The shared build workflows install the package with --no-deps and
light-install when the package has a registry entry, falling back to
the old full [dev] install otherwise. The custom_container input is
removed (the light path makes prebuilt-heavy-deps images unnecessary)
and every workflow now uses uv exclusively: setup-uv replaces the pip
bootstrap, and hf sync runs through uvx. The accelerate integration
test exercises the light path end to end.

Mock hardening the validation surfaced: issubclass()/isinstance()
against a mock answers False instead of raising (scipy>=1.17 probes
`issubclass(cls, torch.Tensor)` whenever torch is in sys.modules).

Every registry file was validated in a fresh venv containing only
doc-builder, the registry's real deps, and the library --no-deps:
all 25 builds (18 libraries + optimum family) pass on first iteration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mishig25

mishig25 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Went with option 2 and overhauled the workflows: registry-driven light installs, custom_container removed, uv-only.

Registry format — each mock_deps/<library>.txt now records the full dependency story of a light doc build:

  • bare lines → mocked at build time (unchanged)
  • real:<spec> → installed for real by the new doc-builder light-install <library> (things imported unguarded, isinstance targets like pyarrow, version-pinned requirements)
  • nodeps:<spec> → installed for real with --no-deps (packages like accelerate/peft/optimum whose own dependency trees pull in torch)

light-install pins bare registry names to the documented library's own declared version ranges (read from its installed metadata), so registry files never chase upstream pins — e.g. it installs tokenizers<=0.23.0,>=0.22.0 for transformers today because that's what transformers@main declares. --check (exit 3 when unregistered) lets scripts decide between light and full installs.

Workflows (build_main_documentation.yml + build_pr_documentation.yml):

  • custom_container input and the container: block are removed — the light path is what the transformers-doc-builder image existed for. ⚠️ Callers that pass custom_container (transformers, accelerate, ...) must drop the input when they bump their workflow SHA past this change, or workflow validation fails on the unknown input.
  • uv-only: astral-sh/setup-uv (SHA-pinned) replaces the pip install -U uv bootstrap, the PIP_OR_UV switch is gone, and the bucket sync runs hf via uvx (which also ends the old hub-version tug-of-war between the venv and the sync step). upload_pr_documentation.yml, delete_old_pr_documentations.yml, and the accelerate integration test are converted too.
  • Install step is now: light-install --check → registered: uv pip install ./pkg --no-deps + doc-builder light-install pkg; unregistered: full [dev] install as before. Non-Python docs (--not_python_module) skip installs, unchanged.
  • accelerate_doc.yml (this repo's own integration test) now runs the light path end to end on every PR.

Validation — every registry file was re-proven in a fresh venv containing only doc-builder + the registry's real deps + the library --no-deps. All 25 builds (18 libraries + the 7 optimum-family builds) pass on the first iteration: transformers 723 pages, diffusers 363, huggingface_hub 104, lerobot 97 (py3.12), timm 77, peft 76, trl 66, openenv 66, datasets 61, optimum.neuron 53, accelerate 48, tokenizers 36, kernels 35, lighteval 27, bitsandbytes 25, optimum-onnx 23, optimum.habana 21, evaluate 20, trackio 20, autotrain 19, optimum.tpu 19, optimum 16, safetensors 10, optimum.intel 9, optimum.executorch 5.

The fresh-venv sweep surfaced one more mock hardening (included): issubclass()/isinstance() against a mock now answers False instead of raising — scipy ≥1.17 probes issubclass(cls, torch.Tensor) whenever it sees torch in sys.modules, which broke lighteval and autotrain with mocked torch.

@mishig25
mishig25 merged commit e60a538 into main Jul 6, 2026
4 checks passed
@mishig25
mishig25 deleted the mock-imports branch July 6, 2026 07:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant