DeepInfer/scripts/copy_site_packages.py
loopyt 788029f80b feat(h20): make the FlagTree image setup reproducible instead of a shell history
Getting the Triton backend to build and run in a vendor image took a dozen non-obvious
steps: a venv because setup.py insists on CONDA_PREFIX, CUDA headers borrowed from the
nvidia-cu13 wheels because the image ships only the runtime, NCCL 2.32.3 because DeepEP
uses ncclCommQueryProperties, libdw/libelf for DeepGEMM's bundled deep_jit, pciutils for
DeepSelect, and a tvm_ffi new enough for tilelang 0.1.15. All of it lived in this session
and in prose, which means the next person starts over.

`setup_flagtree_image.sh` records it in three stages -- env, build, verify -- and is
idempotent, so a failure resumes. It also explains why each workaround exists, since several
look arbitrary otherwise: FlagTree is a Triton fork and needs nothing special, but the C++
extensions around it do.

`copy_site_packages.py` copies distributions by their declared file lists rather than by
name, because pillow installs PIL and pillow.libs, pyzmq installs zmq. It defaults to
keeping the image's pillow: a copied one points at vendored libtiff builds the image does
not have, and PIL.Image then fails to import.

Verified by building into an empty venv rather than the one already set up here: env, build
and verify all pass from scratch, and an end-to-end run from that venv produces the same
eight-rank-agreeing output as before.
2026-10-22 13:20:03 +08:00

146 lines
5.1 KiB
Python
Executable File

"""Copy installed distributions between Python environments, file for file.
Why this exists rather than `pip install`: pip on the H20 host stalls indefinitely on PyPI
(the wheel for `posix_ipc` never finishes downloading), and the packages we need are already
installed and pinned in a reference image. Copying is exact and takes seconds.
Two things it handles that a naive `cp -r` does not:
* A distribution's files are not always under its own name. `pillow` installs `PIL` and
`pillow.libs`, `pyzmq` installs `zmq` and `pyzmq.libs`. The file list comes from the
distribution's metadata, so those sibling directories come along.
* Some packages must *not* be copied even when present, because the target image ships a
version linked against libs the source does not have. `pillow` is the case in hand: copying
11.3 into an image whose `pillow.libs` holds a different libtiff set leaves `PIL.Image`
unable to import. Pass it in `--keep-system` and the target's own copy is reused.
python scripts/copy_site_packages.py <src-site-packages> <dst-site-packages> \\
--packages einops numpy pillow \\
--keep-system pillow
"""
import argparse
import importlib.metadata as md
import shutil
import sys
from pathlib import Path
# Everything `requirements.txt` asks for that the engine imports at load time. torch,
# tilelang, tile-kernels and tvm-ffi are deliberately absent: they are either provided by
# the image or installed by setup_flagtree_image.sh from a reference container, because
# their builds are ABI-sensitive in ways a file copy only hides.
DEFAULT_PACKAGES = [
"einops",
"huggingface_hub",
"ninja",
"numpy",
"orjson",
"pillow",
"posix_ipc",
"psutil",
"pybind11",
"pyzmq",
"requests",
"safetensors",
"scipy",
"sympy",
"tabulate",
"tokenizers",
"tqdm",
"transformers",
"xgrammar",
]
def owned_paths(dist: md.Distribution) -> list[Path]:
"""The top-level entries a distribution installs, plus its metadata directory."""
root = Path(dist.locate_file(""))
found: dict[str, Path] = {}
# top_level.txt names the import roots, which is what we want when they differ from the
# distribution name.
try:
declared = dist.read_text("top_level.txt") or ""
except (FileNotFoundError, UnicodeDecodeError):
declared = ""
for line in declared.splitlines():
name = line.strip()
if name and (root / name).exists():
found[name] = root / name
# Fall back to the file list; single-module distributions have no top_level entry.
if not found:
for f in dist.files or []:
first = f.parts[0]
if first.endswith((".dist-info", ".egg-info", ".data")) or first.startswith(".."):
continue
if (root / first).exists():
found[first] = root / first
found[dist._path.name] = dist._path.resolve()
return list(found.values())
def install(src: Path, dst: Path, name: str, dist: md.Distribution) -> list[str]:
copied = []
for path in owned_paths(dist):
target = dst / path.name
if target.is_symlink() or target.is_file():
target.unlink()
elif target.is_dir():
shutil.rmtree(target)
if path.is_dir() and not path.is_symlink():
shutil.copytree(path, target, symlinks=True)
else:
# `.so` files and metadata are files; copy_file keeps the mode bits, which the
# extension modules need.
shutil.copy2(path, target, follow_symlinks=False)
copied.append(path.name)
return copied
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("src", type=Path, help="source site-packages directory")
parser.add_argument("dst", type=Path, help="destination site-packages directory")
parser.add_argument("--packages", nargs="+", default=DEFAULT_PACKAGES)
parser.add_argument(
"--keep-system",
nargs="*",
default=["pillow"],
help="packages to leave to the destination image, because their vendored libs differ",
)
args = parser.parse_args()
for p in (args.src, args.dst):
if not p.is_dir():
print(f"not a directory: {p}", file=sys.stderr)
return 2
# Reading metadata from `src` without installing it: the finder consults sys.path, so the
# source tree goes first for the life of this process.
sys.path.insert(0, str(args.src))
missing = []
for name in args.packages:
if name in args.keep_system:
print(f"skip {name} (keeping the destination image's copy)")
continue
try:
dist = md.distribution(name)
except md.PackageNotFoundError:
missing.append(name)
print(f"missing {name}")
continue
copied = install(args.src, args.dst, name, dist)
print(f"{dist.version:<10}{name}: {', '.join(copied)}")
if missing:
print(f"\nnot found in {args.src}: {', '.join(missing)}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())