Skip to content

Commit 3edf8f3

Browse files
committed
Move the build report into a standalone script
Per review: embedded in a bash heredoc the python was invisible to flake8, mypy and pyfmt. As .github/scripts/build_report.py it is covered by all three (mypy.ini gets an import-not-found exemption, since torch is not installed in the lint environment). Being a real script file also removes the reason the caller had to cd out of the repo first: sys.path[0] is now the script's own directory rather than the cwd, so `import torch` can no longer pick up the pytorch source checkout the validation runs from.
1 parent a659086 commit 3edf8f3

3 files changed

Lines changed: 164 additions & 73 deletions

File tree

.github/scripts/build_report.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#!/usr/bin/env python3
2+
"""Report the versions and sizes of the binaries a validation run installed.
3+
4+
Emits a small markdown table that validate_binaries.sh prints to the job log and
5+
appends to the job summary. Values are read from the installed packages rather
6+
than from the build matrix, so a mismatch between what was requested and what
7+
pip resolved is visible in the report.
8+
9+
Two sizes are reported because they answer different questions:
10+
11+
* wheel -- the compressed download size, i.e. what a user pulls from the
12+
index. Only pip knows it, so the caller passes it in; it is
13+
absent on the uv/wheel-variants path and when the wheel was
14+
already satisfied.
15+
* installed -- unpacked bytes on disk, measured here from the installed
16+
package, so it is available on every install path and every OS.
17+
18+
Nothing in here may fail the validation job: every lookup that depends on how
19+
torch was built (CUDA, cuDNN, NCCL) degrades to "-" instead of raising.
20+
21+
Note on imports: this is deliberately a script file rather than a heredoc piped
22+
to python. For a script, sys.path[0] is the script's own directory, so `import
23+
torch` cannot pick up the pytorch source checkout the validation runs from --
24+
which a `python -` heredoc would, since that puts the cwd on sys.path.
25+
26+
Usage:
27+
build_report.py --target-os linux --python-version 3.12 \
28+
--gpu-arch-type cuda --gpu-arch-version 12.8 \
29+
--torch-wheel-mb 812.4 --torchvision-wheel-mb 8.1
30+
"""
31+
32+
from __future__ import annotations
33+
34+
import argparse
35+
import os
36+
from types import ModuleType
37+
38+
39+
def installed_size_mb(module: ModuleType) -> str:
40+
"""Total size of a package directory on disk, formatted as MB.
41+
42+
Files that vanish or cannot be stat'd mid-walk are skipped rather than
43+
aborting the report.
44+
"""
45+
path = getattr(module, "__file__", None)
46+
if not path:
47+
return "-"
48+
49+
total = 0
50+
for dirpath, _, filenames in os.walk(os.path.dirname(path)):
51+
for filename in filenames:
52+
try:
53+
total += os.path.getsize(os.path.join(dirpath, filename))
54+
except OSError:
55+
continue
56+
return f"{total / 1024 / 1024:.1f} MB"
57+
58+
59+
def format_mb(value: str) -> str:
60+
"""Render a caller-supplied megabyte figure, or "-" when it is unknown."""
61+
return f"{value} MB" if value else "-"
62+
63+
64+
def cudnn_version(torch: ModuleType) -> str:
65+
"""Decode torch's packed cuDNN version integer, e.g. 91002 -> 9.10.2."""
66+
try:
67+
packed = torch.backends.cudnn.version()
68+
except Exception:
69+
return "-"
70+
if not packed:
71+
return "-"
72+
return f"{packed // 10000}.{packed % 10000 // 100}.{packed % 100}"
73+
74+
75+
def nccl_version(torch: ModuleType) -> str:
76+
"""Format torch's NCCL version tuple, e.g. (2, 30, 7) -> 2.30.7."""
77+
try:
78+
return ".".join(str(part) for part in torch.cuda.nccl.version())
79+
except Exception:
80+
return "-"
81+
82+
83+
def collect_rows(args: argparse.Namespace) -> list[tuple[str, str]]:
84+
arch = args.gpu_arch_type
85+
if args.gpu_arch_version:
86+
arch = f"{arch} {args.gpu_arch_version}"
87+
rows = [("build", f"{args.target_os} / py{args.python_version} / {arch}")]
88+
89+
try:
90+
import torch
91+
except Exception as exc:
92+
rows.append(("torch", f"import failed: {exc}"))
93+
else:
94+
rows.extend(
95+
[
96+
("torch", torch.__version__),
97+
("torch wheel", format_mb(args.torch_wheel_mb)),
98+
("torch installed", installed_size_mb(torch)),
99+
("CUDA", torch.version.cuda or "-"),
100+
("cuDNN", cudnn_version(torch)),
101+
("NCCL", nccl_version(torch)),
102+
]
103+
)
104+
105+
try:
106+
import torchvision
107+
except Exception:
108+
rows.append(("torchvision", "-"))
109+
else:
110+
rows.extend(
111+
[
112+
("torchvision", torchvision.__version__),
113+
("torchvision wheel", format_mb(args.torchvision_wheel_mb)),
114+
("torchvision installed", installed_size_mb(torchvision)),
115+
]
116+
)
117+
118+
return rows
119+
120+
121+
def parse_args() -> argparse.Namespace:
122+
parser = argparse.ArgumentParser(description=__doc__)
123+
parser.add_argument("--target-os", default="?")
124+
parser.add_argument("--python-version", default="?")
125+
parser.add_argument("--gpu-arch-type", default="cpu")
126+
parser.add_argument("--gpu-arch-version", default="")
127+
parser.add_argument(
128+
"--torch-wheel-mb",
129+
default="",
130+
help="Compressed torch wheel size in MB, as read from pip's output",
131+
)
132+
parser.add_argument(
133+
"--torchvision-wheel-mb",
134+
default="",
135+
help="Compressed torchvision wheel size in MB, as read from pip's output",
136+
)
137+
return parser.parse_args()
138+
139+
140+
def main() -> None:
141+
rows = collect_rows(parse_args())
142+
print("| field | value |")
143+
print("| --- | --- |")
144+
for field, value in rows:
145+
print(f"| {field} | {value} |")
146+
147+
148+
if __name__ == "__main__":
149+
main()

.github/scripts/validate_binaries.sh

Lines changed: 10 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -314,83 +314,20 @@ check_wheel_size() {
314314
# wheels and exists to enforce a ceiling: this runs for every build on every OS
315315
# and never fails, so windows/macos/ROCm sizes are visible too.
316316
#
317-
# Two sizes are reported because they answer different questions:
318-
# wheel -- compressed download size, what users pull from the index.
319-
# Only available when pip printed it (absent on the uv/variants
320-
# path and when the wheel was already satisfied).
321-
# installed -- unpacked bytes on disk, measured from the installed package,
322-
# so it is available on every install path.
323-
#
324-
# Runs while the env is still active: cleanup_conda_env removes it on non-linux.
325-
# Values are read from the installed packages rather than from the matrix, so a
326-
# mismatch between what was requested and what pip resolved shows up here.
317+
# The report itself is built by build_report.py; see that script for what the
318+
# two size figures mean. Runs while the env is still active, because
319+
# cleanup_conda_env removes it on non-linux.
327320
write_build_report() {
328321
local torch_wheel_mb="${1:-}" vision_wheel_mb="${2:-}"
329322
local report
330323

331-
# cd out of the repo: this runs from the pytorch/pytorch checkout, where
332-
# `import torch` would pick up the source tree instead of the install.
333-
report=$(cd "${TMPDIR:-/tmp}" 2>/dev/null || cd "${HOME}"; "${PYTHON_RUN}" - \
334-
"${TARGET_OS}" "${MATRIX_PYTHON_VERSION:-?}" "${MATRIX_GPU_ARCH_TYPE:-cpu}" \
335-
"${MATRIX_GPU_ARCH_VERSION:-}" "${torch_wheel_mb}" "${vision_wheel_mb}" <<'PY'
336-
import os
337-
import sys
338-
339-
target_os, py, arch_type, arch_ver, torch_wheel, vision_wheel = sys.argv[1:7]
340-
341-
342-
def installed_mb(mod):
343-
root = os.path.dirname(mod.__file__)
344-
total = 0
345-
for dirpath, _, names in os.walk(root):
346-
for n in names:
347-
try:
348-
total += os.path.getsize(os.path.join(dirpath, n))
349-
except OSError:
350-
pass
351-
return "%.1f MB" % (total / 1024 / 1024)
352-
353-
354-
def mb(value):
355-
return "%s MB" % value if value else "-"
356-
357-
358-
rows = [("build", "%s / py%s / %s%s" % (target_os, py, arch_type,
359-
" " + arch_ver if arch_ver else ""))]
360-
361-
try:
362-
import torch
363-
rows.append(("torch", torch.__version__))
364-
rows.append(("torch wheel", mb(torch_wheel)))
365-
rows.append(("torch installed", installed_mb(torch)))
366-
rows.append(("CUDA", torch.version.cuda or "-"))
367-
try:
368-
v = torch.backends.cudnn.version()
369-
rows.append(("cuDNN", "%d.%d.%d" % (v // 10000, v % 10000 // 100, v % 100)
370-
if v else "-"))
371-
except Exception:
372-
rows.append(("cuDNN", "-"))
373-
try:
374-
rows.append(("NCCL", ".".join(str(p) for p in torch.cuda.nccl.version())))
375-
except Exception:
376-
rows.append(("NCCL", "-"))
377-
except Exception as e: # never fail the build over a report
378-
rows.append(("torch", "import failed: %s" % e))
379-
380-
try:
381-
import torchvision
382-
rows.append(("torchvision", torchvision.__version__))
383-
rows.append(("torchvision wheel", mb(vision_wheel)))
384-
rows.append(("torchvision installed", installed_mb(torchvision)))
385-
except Exception:
386-
rows.append(("torchvision", "-"))
387-
388-
print("| field | value |")
389-
print("| --- | --- |")
390-
for k, v in rows:
391-
print("| %s | %s |" % (k, v))
392-
PY
393-
) || report="| field | value |
324+
report=$("${PYTHON_RUN}" "${SCRIPT_DIR}/build_report.py" \
325+
--target-os "${TARGET_OS}" \
326+
--python-version "${MATRIX_PYTHON_VERSION:-?}" \
327+
--gpu-arch-type "${MATRIX_GPU_ARCH_TYPE:-cpu}" \
328+
--gpu-arch-version "${MATRIX_GPU_ARCH_VERSION:-}" \
329+
--torch-wheel-mb "${torch_wheel_mb}" \
330+
--torchvision-wheel-mb "${vision_wheel_mb}") || report="| field | value |
394331
| --- | --- |
395332
| report | failed to collect |"
396333

mypy.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ disable_error_code = attr-defined, return-value
1515
[mypy-.github.scripts.run_with_env_secrets]
1616
disable_error_code = attr-defined, return-value, union-attr
1717

18+
[mypy-.github.scripts.build_report]
19+
# torch/torchvision are not installed in the lint environment; the script
20+
# imports them defensively at runtime and degrades when they are absent.
21+
disable_error_code = import-not-found, import-untyped
22+
1823
[mypy-aws.lambda.whl_metadata_upload_pep658.lambda_function]
1924
disable_error_code = unused-ignore, import-not-found
2025

0 commit comments

Comments
 (0)