File size: 8,119 Bytes
e479c46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
#!/bin/bash
# Build missing aarch64 dGPU wheels before the root pyproject's path sources are
# resolved by uv sync. Dependency pins are the source of truth; the pyproject
# path sources are updated to point at the matching generated wheel names.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
PYPROJECT="$REPO_ROOT/pyproject.toml"
WHEEL_DIR="$SCRIPT_DIR/wheels"

mkdir -p "$WHEEL_DIR"

# Target CPython derived from the project's requires-python, so the build venv
# and the wheel's cpXY tag track the pinned interpreter. A literal cp310/python3.10
# silently builds an unusable wheel after a runtime bump (e.g. the py3.12 migration).
resolve_python_version() {
    python3 - "$PYPROJECT" <<'PY'
import pathlib
import re
import sys

text = pathlib.Path(sys.argv[1]).read_text()
# Accept both TOML quote styles (" and ', written as hex \x22 / \x27).
m = re.search(r"requires-python\s*=\s*[\x22\x27]([^\x22\x27]+)[\x22\x27]", text)
if m is None:
    raise SystemExit("ERROR: requires-python not found in pyproject.toml")
v = re.search(r"(\d+)\.(\d+)", m.group(1))
if v is None:
    raise SystemExit(f"ERROR: could not parse a CPython version from {m.group(1)!r}")
print(f"{v.group(1)}.{v.group(2)}")
PY
}

PY_VERSION="$(resolve_python_version)"
PYTHON_BIN="python${PY_VERSION}"
CP_TAG="cp${PY_VERSION/./}"

resolve_dependency_version() {
    python3 - "$PYPROJECT" "$1" <<'PY'
import pathlib
import re
import sys

pyproject_path = pathlib.Path(sys.argv[1])
package = sys.argv[2]
text = pyproject_path.read_text()

dep_line = None
for line in text.splitlines():
    if f'"{package}==' in line and "platform_machine" in line and "aarch64" in line:
        dep_line = line
        break
if dep_line is None:
    for line in text.splitlines():
        if f'"{package}==' in line:
            dep_line = line
            break
if dep_line is None:
    raise SystemExit(f"ERROR: pinned project dependency not found for {package}")

dep_match = re.search(rf'"{re.escape(package)}==([^";]+)', dep_line)
if dep_match is None:
    raise SystemExit(f"ERROR: could not parse pinned dependency version for {package}")

print(dep_match.group(1))
PY
}

update_pyproject_path_source() {
    python3 - "$PYPROJECT" "$1" "$2" <<'PY'
import pathlib
import re
import sys

pyproject_path = pathlib.Path(sys.argv[1])
package = sys.argv[2]
expected_path = sys.argv[3]
text = pyproject_path.read_text()

source_block_match = re.search(
    rf"^{re.escape(package)}\s*=\s*\[(.*?)^\]",
    text,
    flags=re.MULTILINE | re.DOTALL,
)
if source_block_match is None:
    raise SystemExit(f"ERROR: [tool.uv.sources] entry not found for {package}")

block_start, block_end = source_block_match.span(1)
block = source_block_match.group(1)

entry_pattern = re.compile(r"\{[^\n]*path\s*=\s*\"([^\"]+)\"[^\n]*\}")
matches = list(entry_pattern.finditer(block))
aarch64_matches = [
    match
    for match in matches
    if "platform_machine == 'aarch64'" in match.group(0)
    and "scripts/deployment/dgpu/wheels/" in match.group(1)
]
if len(aarch64_matches) != 1:
    raise SystemExit(
        f"ERROR: expected exactly one dGPU aarch64 path source for {package}, "
        f"found {len(aarch64_matches)}"
    )

match = aarch64_matches[0]
current_path = match.group(1)
if current_path == expected_path:
    print(f"{package} path source already points to {expected_path}")
    raise SystemExit(0)

entry = match.group(0)
updated_entry = entry.replace(f'path = "{current_path}"', f'path = "{expected_path}"')
updated_block = block[: match.start()] + updated_entry + block[match.end() :]
updated_text = text[:block_start] + updated_block + text[block_end:]
pyproject_path.write_text(updated_text)
print(f"Updated {package} path source: {current_path} -> {expected_path}")
PY
}

wheel_path_for() {
    package_prefix="$1"
    version="$2"
    printf "%s/%s-%s-${CP_TAG}-${CP_TAG}-linux_aarch64.whl" "$WHEEL_DIR" "$package_prefix" "$version"
}

# flash-attn now ships as official cu12torch2.9 cp312 release wheels for both
# x86_64 and aarch64 (see [tool.uv.sources] in pyproject.toml), so the dGPU
# bootstrap only owns the aarch64 torchcodec wheel (no PyPI aarch64 wheel).
TORCHCODEC_VERSION="$(resolve_dependency_version "torchcodec")"
TORCHCODEC_SOURCE_VERSION="$(printf "%s" "$TORCHCODEC_VERSION" | sed -E 's/a[0-9]+$//')"

TORCHCODEC_WHEEL="$(wheel_path_for "torchcodec" "$TORCHCODEC_VERSION")"
TORCHCODEC_PATH="scripts/deployment/dgpu/wheels/$(basename "$TORCHCODEC_WHEEL")"

echo "Expected dGPU aarch64 wheels from dependency pins:"
echo "  torchcodec==$TORCHCODEC_VERSION -> $TORCHCODEC_PATH"

update_pyproject_path_source "torchcodec" "$TORCHCODEC_PATH"

if [ "${DGPU_WHEEL_BOOTSTRAP_VALIDATE_ONLY:-0}" = "1" ]; then
    exit 0
fi

if [ "$(uname -m)" != "aarch64" ]; then
    echo "dGPU wheel bootstrap is only needed on aarch64; skipping."
    exit 0
fi

if [ -f "$TORCHCODEC_WHEEL" ]; then
    echo "Matching dGPU aarch64 torchcodec wheel already exists; skipping source build."
    exit 0
fi

if ! command -v uv &> /dev/null; then
    echo "ERROR: uv is required to bootstrap dGPU wheels." >&2
    exit 1
fi

BUILD_VENV="${DGPU_WHEEL_BUILD_VENV:-/tmp/gr00t-dgpu-wheel-build-venv}"
BUILD_PYTHON="$BUILD_VENV/bin/python"
TMP_BUILD_DIRS=()
trap 'rm -rf "$BUILD_VENV"; for _d in "${TMP_BUILD_DIRS[@]:-}"; do rm -rf "$_d"; done' EXIT

rm -rf "$BUILD_VENV"
"$PYTHON_BIN" -m venv "$BUILD_VENV"

uv_pip_install_retry() {
    for attempt in 1 2 3 4 5; do
        if uv pip install "$@"; then
            return 0
        fi
        sleep $((attempt * 10))
    done
    return 1
}

# Build against the project's pinned torch / triton / numpy (derived from
# pyproject.toml) so the generated wheel's ABI matches the runtime stack.
# Hardcoded versions silently produce a wheel for the wrong torch after a bump.
TORCH_VERSION="$(resolve_dependency_version "torch")"
TRITON_VERSION="$(resolve_dependency_version "triton")"
NUMPY_VERSION="$(resolve_dependency_version "numpy")"

uv_pip_install_retry --python "$BUILD_PYTHON" \
    --index-url https://download.pytorch.org/whl/cu128 \
    --extra-index-url https://pypi.org/simple \
    "torch==${TORCH_VERSION}" "triton==${TRITON_VERSION}" "numpy==${NUMPY_VERSION}"
uv_pip_install_retry --python "$BUILD_PYTHON" pip setuptools wheel packaging ninja

SITE_PKGS=$("$BUILD_PYTHON" - <<'PY'
import site

print(site.getsitepackages()[0])
PY
)
NVIDIA_LIB_DIRS="$(find "${SITE_PKGS}/nvidia" -name "lib" -type d 2>/dev/null | tr '\n' ':')"
export LD_LIBRARY_PATH="/usr/local/cuda/lib64:${SITE_PKGS}/torch/lib:${NVIDIA_LIB_DIRS}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export CUDA_HOME=/usr/local/cuda
export CUDA_PATH=/usr/local/cuda
export CPATH="${CUDA_HOME}/include:${CPATH:-}"
export C_INCLUDE_PATH="${CUDA_HOME}/include:${C_INCLUDE_PATH:-}"
export CPLUS_INCLUDE_PATH="${CUDA_HOME}/include:${CPLUS_INCLUDE_PATH:-}"
export MAX_JOBS="${MAX_JOBS:-$(nproc)}"
export NVCC_THREADS="${NVCC_THREADS:-1}"
export CMAKE_BUILD_PARALLEL_LEVEL="${CMAKE_BUILD_PARALLEL_LEVEL:-$(nproc)}"

build_torchcodec() {
    if [ -f "$TORCHCODEC_WHEEL" ]; then
        echo "torchcodec wheel already exists: $TORCHCODEC_WHEEL"
        return
    fi

    echo "No dGPU aarch64 torchcodec wheel found; building from source..."
    rm -rf /tmp/torchcodec
    TMP_BUILD_DIRS+=(/tmp/torchcodec)
    git clone --depth 1 --branch "v${TORCHCODEC_SOURCE_VERSION}" \
        https://github.com/pytorch/torchcodec.git /tmp/torchcodec
    rm -rf /tmp/torchcodec/.git

    I_CONFIRM_THIS_IS_NOT_A_LICENSE_VIOLATION=1 "$BUILD_PYTHON" -m pip wheel \
        --no-build-isolation \
        --no-deps \
        --wheel-dir "$WHEEL_DIR" \
        /tmp/torchcodec

    if [ ! -f "$TORCHCODEC_WHEEL" ]; then
        echo "ERROR: torchcodec source build did not produce expected wheel:" >&2
        echo "  $TORCHCODEC_WHEEL" >&2
        echo "Available torchcodec wheels:" >&2
        find "$WHEEL_DIR" -maxdepth 1 -name 'torchcodec-*.whl' -print >&2
        exit 1
    fi
}

cd "$REPO_ROOT"
build_torchcodec

echo "dGPU aarch64 wheel bootstrap complete:"
ls -lh "$TORCHCODEC_WHEEL"