Skip to content

Commit 9bfece5

Browse files
committed
feat: add tarball build pipeline for process injection
Add self-contained runtime tarball that can be injected into any base image via dockerArgs, replacing the need for pre-built Docker images. - Add build-tarball.sh using uv for Python version management - Add bootstrap.sh entry point for injected runtime - Add Makefile tarball/tarball-test/tarball-test-local targets - Add release-tarball.yml CI workflow triggered on release - Unify all base images on Python 3.11 (matching PyTorch runtime) - Fix VERSION regex to handle spaces around = in version.py - Add --platform linux/amd64 to test targets for Apple Silicon - Update dependency_installer for tarball-aware install paths - Add tarball constants (TARBALL_URL_TEMPLATE, TARBALL_INSTALL_DIR)
1 parent 81319a1 commit 9bfece5

8 files changed

Lines changed: 379 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,54 @@ jobs:
128128
echo "Testing LB handler in Docker environment..."
129129
docker run --rm flash-lb-cpu:test ./test-lb-handler.sh
130130
131+
tarball:
132+
runs-on: ubuntu-latest
133+
if: github.event_name == 'pull_request'
134+
steps:
135+
- name: Checkout repository
136+
uses: actions/checkout@v4
137+
138+
- name: Set up uv
139+
uses: astral-sh/setup-uv@v4
140+
with:
141+
enable-cache: true
142+
143+
- name: Build tarball
144+
env:
145+
PYTHON_VERSION: "3.11"
146+
run: bash scripts/build-tarball.sh
147+
148+
- name: Test tarball in bare ubuntu container
149+
run: |
150+
TARBALL=$(ls dist/flash-worker-v*-py3.11-linux-x86_64.tar.gz)
151+
docker run --rm -v "$(pwd)/dist:/dist" ubuntu:22.04 \
152+
bash -c "tar xzf /dist/$(basename $TARBALL) -C /opt && /opt/flash-worker/bootstrap.sh --test"
153+
154+
- name: Upload tarball artifact
155+
uses: actions/upload-artifact@v4
156+
with:
157+
name: flash-worker-tarball
158+
path: dist/flash-worker-v*-py3.11-linux-x86_64.tar.gz
159+
retention-days: 30
160+
overwrite: true
161+
162+
- name: Post artifact link on PR
163+
env:
164+
GH_TOKEN: ${{ github.token }}
165+
run: |
166+
TARBALL=$(basename dist/flash-worker-v*-py3.11-linux-x86_64.tar.gz)
167+
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
168+
BODY="**Tarball artifact:** [\`${TARBALL}\`](${RUN_URL}#artifacts)
169+
170+
To test: \`FLASH_WORKER_TARBALL_URL=<download-url> flash deploy\`"
171+
172+
# Delete previous tarball comments to keep PR clean
173+
gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
174+
--jq '.[] | select(.body | contains("Tarball artifact:")) | .id' | \
175+
xargs -I{} gh api -X DELETE "repos/${{ github.repository }}/issues/comments/{}" 2>/dev/null || true
176+
177+
gh pr comment "${{ github.event.pull_request.number }}" --body "$BODY"
178+
131179
docker-validation:
132180
runs-on: ubuntu-latest
133181
needs: [test, lint, docker-test, docker-test-lb-cpu]
@@ -229,6 +277,13 @@ jobs:
229277
cache-from: type=gha,scope=gpu
230278
cache-to: type=gha,mode=max,scope=gpu
231279

280+
tarball-release:
281+
needs: [release]
282+
if: needs.release.outputs.release_created
283+
uses: ./.github/workflows/release-tarball.yml
284+
with:
285+
tag_name: ${{ needs.release.outputs.tag_name }}
286+
232287
docker-prod-cpu:
233288
runs-on: ubuntu-latest
234289
needs: [release]
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Release Tarball
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
dry_run:
7+
description: "Dry run (build but don't upload)"
8+
required: false
9+
default: "false"
10+
type: boolean
11+
12+
# Triggered by release job in ci.yml via workflow_call
13+
# or manually via workflow_dispatch
14+
workflow_call:
15+
inputs:
16+
tag_name:
17+
required: true
18+
type: string
19+
20+
permissions:
21+
contents: write
22+
23+
jobs:
24+
build-tarball:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- name: Checkout repository
28+
uses: actions/checkout@v4
29+
30+
- name: Set up uv
31+
uses: astral-sh/setup-uv@v4
32+
with:
33+
enable-cache: true
34+
35+
- name: Build tarball
36+
env:
37+
PYTHON_VERSION: "3.11"
38+
run: bash scripts/build-tarball.sh
39+
40+
- name: Test tarball in bare ubuntu container
41+
run: |
42+
TARBALL=$(ls dist/flash-worker-v*-py3.11-linux-x86_64.tar.gz)
43+
docker run --rm -v "$(pwd)/dist:/dist" ubuntu:22.04 \
44+
bash -c "tar xzf /dist/$(basename $TARBALL) -C /opt && /opt/flash-worker/bootstrap.sh --test"
45+
46+
- name: Upload tarball to GitHub Release
47+
if: inputs.dry_run != 'true' && inputs.tag_name != ''
48+
env:
49+
GH_TOKEN: ${{ github.token }}
50+
run: |
51+
TARBALL=$(ls dist/flash-worker-v*-py3.11-linux-x86_64.tar.gz)
52+
gh release upload "${{ inputs.tag_name }}" "$TARBALL" --clobber
53+
54+
- name: Upload tarball as artifact (for dry runs)
55+
if: inputs.dry_run == 'true' || inputs.tag_name == ''
56+
uses: actions/upload-artifact@v4
57+
with:
58+
name: flash-worker-tarball
59+
path: dist/flash-worker-v*-py3.11-linux-x86_64.tar.gz
60+
retention-days: 7

Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ IMAGE = runpod/flash
22
TAG = $(or $(FLASH_IMAGE_TAG),local)
33
FULL_IMAGE = $(IMAGE):$(TAG)
44
FULL_IMAGE_CPU = $(IMAGE)-cpu:$(TAG)
5+
VERSION = $(shell python3 -c "import re; print(re.search(r'__version__\s*=\s*\"([^\"]+)\"', open('src/version.py').read()).group(1))")
6+
# Must match base image Python: pytorch:2.9.1-cuda12.8-cudnn9-runtime and python:3.11-slim
7+
TARBALL_PYTHON_VERSION ?= 3.11
58

69
# Detect host platform for local builds
710
ARCH := $(shell uname -m)
@@ -56,6 +59,27 @@ clean: # Remove build artifacts and cache files
5659
find . -type f -name "*.pyc" -delete
5760
find . -type f -name "*.pkl" -delete
5861

62+
# Tarball targets (process-injectable runtime)
63+
tarball: # Build self-contained runtime tarball (runs in Docker, linux/amd64)
64+
docker run --rm --platform linux/amd64 \
65+
-e PYTHON_VERSION=$(TARBALL_PYTHON_VERSION) \
66+
-e UV_CACHE_DIR=/workspace/dist/.uv-cache \
67+
-v $(PWD):/workspace -w /workspace \
68+
python:3.11-slim \
69+
bash -c 'apt-get update -qq && apt-get install -y -qq curl > /dev/null 2>&1 && pip install uv -q && bash scripts/build-tarball.sh'
70+
71+
tarball-test: tarball # Test tarball in bare ubuntu container
72+
docker run --rm --platform linux/amd64 -v $(PWD)/dist:/dist ubuntu:22.04 \
73+
bash -c 'tar xzf /dist/flash-worker-v$(VERSION)-py$(TARBALL_PYTHON_VERSION)-linux-x86_64.tar.gz -C /opt && /opt/flash-worker/bootstrap.sh --test'
74+
75+
tarball-test-local: # Test tarball injection with mounted file (no rebuild)
76+
docker run --rm --platform linux/amd64 \
77+
-v $(PWD)/dist/flash-worker-v$(VERSION)-py$(TARBALL_PYTHON_VERSION)-linux-x86_64.tar.gz:/tmp/flash-worker.tar.gz \
78+
ubuntu:22.04 \
79+
bash -c 'set -e; FW_DIR=/opt/flash-worker; mkdir -p $$FW_DIR; \
80+
tar xzf /tmp/flash-worker.tar.gz -C $$FW_DIR --strip-components=1; \
81+
$$FW_DIR/bootstrap.sh --test'
82+
5983
setup: dev # Initialize project and sync dependencies
6084
@echo "Setup complete. Development environment ready."
6185

scripts/bootstrap.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/bin/sh
2+
# Flash Worker bootstrap -- entry point for process-injected runtime.
3+
# Launched by dockerArgs after tarball extraction.
4+
set -e
5+
6+
FW_DIR="$(cd "$(dirname "$0")" && pwd)"
7+
8+
# Self-test mode (used by tarball-test targets)
9+
if [ "$1" = "--test" ]; then
10+
echo "Flash Worker bootstrap self-test"
11+
echo "FW_DIR: $FW_DIR"
12+
echo "Python: $("$FW_DIR/python/bin/python3" --version)"
13+
echo "uv: $("$FW_DIR/uv" --version)"
14+
"$FW_DIR/venv/bin/python" -c "import pydantic; print(f'pydantic {pydantic.__version__}')"
15+
"$FW_DIR/venv/bin/python" -c "import fastapi; print(f'fastapi {fastapi.__version__}')"
16+
echo "Version: $(cat "$FW_DIR/.version")"
17+
echo "Self-test passed"
18+
exit 0
19+
fi
20+
21+
# Isolated flash-worker environment
22+
export PATH="$FW_DIR/venv/bin:$FW_DIR/python/bin:$FW_DIR:$PATH"
23+
export VIRTUAL_ENV="$FW_DIR/venv"
24+
PYTHON_MINOR=$("$FW_DIR/python/bin/python3" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
25+
export PYTHONPATH="$FW_DIR/src:$VIRTUAL_ENV/lib/python${PYTHON_MINOR}/site-packages${PYTHONPATH:+:$PYTHONPATH}"
26+
27+
# Signal tarball mode for dependency installer
28+
export FLASH_WORKER_INSTALL_DIR="$FW_DIR"
29+
30+
# Mode detection (same contract as Docker images)
31+
ENDPOINT_TYPE="${FLASH_ENDPOINT_TYPE:-qb}"
32+
33+
if [ "$ENDPOINT_TYPE" = "lb" ]; then
34+
exec uvicorn lb_handler:app \
35+
--host 0.0.0.0 \
36+
--port 80 \
37+
--timeout-keep-alive 600 \
38+
--app-dir "$FW_DIR/src"
39+
else
40+
exec python3 "$FW_DIR/src/handler.py"
41+
fi

scripts/build-tarball.sh

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#!/usr/bin/env bash
2+
# Build a self-contained flash-worker tarball for process injection.
3+
# Output: dist/flash-worker-v{VERSION}-py{PYTHON_VERSION}-linux-x86_64.tar.gz
4+
set -euo pipefail
5+
6+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
8+
9+
# Read version from source
10+
VERSION=$(python3 -c "
11+
import re
12+
text = open('$REPO_ROOT/src/version.py').read()
13+
print(re.search(r'__version__\\s*=\\s*\"([^\"]+)\"', text).group(1))
14+
")
15+
echo "Building flash-worker tarball v${VERSION}"
16+
17+
# Configuration
18+
PYTHON_VERSION="${PYTHON_VERSION:-3.11}"
19+
20+
# Validate Python version against project requirement (>=3.10, <3.15)
21+
PY_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2)
22+
if [ "$PY_MINOR" -lt 10 ] || [ "$PY_MINOR" -ge 15 ]; then
23+
echo "ERROR: Python ${PYTHON_VERSION} is outside project requirement (>=3.10, <3.15)"
24+
exit 1
25+
fi
26+
27+
UV_VERSION="0.7.19"
28+
UV_URL="https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-x86_64-unknown-linux-gnu.tar.gz"
29+
30+
# Build in container-local tmpdir to avoid macOS case-insensitive filesystem issues
31+
BUILD_DIR="/tmp/flash-worker-build"
32+
TARBALL_ROOT="$BUILD_DIR/flash-worker"
33+
OUTPUT_DIR="$REPO_ROOT/dist"
34+
TARBALL_NAME="flash-worker-v${VERSION}-py${PYTHON_VERSION}-linux-x86_64.tar.gz"
35+
36+
# Clean previous build
37+
rm -rf "$BUILD_DIR"
38+
mkdir -p "$TARBALL_ROOT" "$OUTPUT_DIR" "$OUTPUT_DIR/.cache"
39+
40+
# 1. Install Python via uv (handles version resolution and caching)
41+
echo "Installing Python ${PYTHON_VERSION} via uv..."
42+
uv python install "$PYTHON_VERSION"
43+
PYTHON_BIN=$(uv python find --python-preference only-managed "$PYTHON_VERSION")
44+
PYTHON_INSTALL_DIR=$(cd "$(dirname "$PYTHON_BIN")/.." && pwd -P)
45+
cp -r "$PYTHON_INSTALL_DIR" "$TARBALL_ROOT/python"
46+
47+
# Verify installation
48+
if [ ! -f "$TARBALL_ROOT/python/bin/python3" ]; then
49+
echo "ERROR: Python installation failed - python3 binary not found"
50+
exit 1
51+
fi
52+
PYTHON_FULL_VERSION=$("$TARBALL_ROOT/python/bin/python3" -c "import sys; v=sys.version_info; print(f'{v.major}.{v.minor}.{v.micro}')")
53+
echo " Python ${PYTHON_FULL_VERSION} installed"
54+
55+
# 2. Download uv static binary
56+
echo "Downloading uv ${UV_VERSION}..."
57+
if [ -f "$OUTPUT_DIR/.cache/uv-${UV_VERSION}.tar.gz" ]; then
58+
echo " Using cached uv download"
59+
tar xzf "$OUTPUT_DIR/.cache/uv-${UV_VERSION}.tar.gz" -C "$TARBALL_ROOT" --no-same-owner --strip-components=1 "uv-x86_64-unknown-linux-gnu/uv" 2>/dev/null || true
60+
else
61+
curl -fsSL "$UV_URL" -o "$OUTPUT_DIR/.cache/uv-${UV_VERSION}.tar.gz"
62+
tar xzf "$OUTPUT_DIR/.cache/uv-${UV_VERSION}.tar.gz" -C "$TARBALL_ROOT" --no-same-owner --strip-components=1 "uv-x86_64-unknown-linux-gnu/uv" 2>/dev/null || true
63+
fi
64+
chmod +x "$TARBALL_ROOT/uv"
65+
66+
# 3. Create venv using portable Python
67+
echo "Creating virtual environment..."
68+
"$TARBALL_ROOT/python/bin/python3" -m venv "$TARBALL_ROOT/venv"
69+
70+
# Fix venv symlinks to be relative (python -m venv creates absolute symlinks)
71+
cd "$TARBALL_ROOT/venv/bin"
72+
for link in python python3 python3.*; do
73+
[ -L "$link" ] || continue
74+
target=$(readlink "$link")
75+
case "$target" in
76+
/*) # Absolute path — make relative to ../../python/bin/
77+
basename=$(basename "$target")
78+
ln -sf "../../python/bin/$basename" "$link"
79+
;;
80+
esac
81+
done
82+
cd "$REPO_ROOT"
83+
84+
# 4. Export and install production dependencies
85+
echo "Installing production dependencies..."
86+
cd "$REPO_ROOT"
87+
# Use the host uv to export requirements (it reads pyproject.toml/uv.lock)
88+
uv export --format requirements-txt --no-dev --no-hashes > "$BUILD_DIR/requirements.txt"
89+
90+
# Install into the tarball's venv using the tarball's uv
91+
"$TARBALL_ROOT/uv" pip install \
92+
--python "$TARBALL_ROOT/venv/bin/python" \
93+
-r "$BUILD_DIR/requirements.txt"
94+
95+
# 5. Copy source files
96+
echo "Copying source files..."
97+
cp -r "$REPO_ROOT/src/"*.py "$TARBALL_ROOT/src/" 2>/dev/null || true
98+
mkdir -p "$TARBALL_ROOT/src"
99+
for f in "$REPO_ROOT/src/"*.py; do
100+
[ -f "$f" ] && cp "$f" "$TARBALL_ROOT/src/"
101+
done
102+
# Copy test scripts (used by --test flag)
103+
for f in "$REPO_ROOT/src/"*.sh; do
104+
[ -f "$f" ] && cp "$f" "$TARBALL_ROOT/src/" && chmod +x "$TARBALL_ROOT/src/$(basename "$f")"
105+
done
106+
# Copy test JSON files
107+
if [ -d "$REPO_ROOT/src/tests" ]; then
108+
cp -r "$REPO_ROOT/src/tests" "$TARBALL_ROOT/src/tests"
109+
fi
110+
111+
# 6. Copy bootstrap script
112+
cp "$REPO_ROOT/scripts/bootstrap.sh" "$TARBALL_ROOT/bootstrap.sh"
113+
chmod +x "$TARBALL_ROOT/bootstrap.sh"
114+
115+
# 7. Write version file for cache invalidation
116+
echo "$VERSION" > "$TARBALL_ROOT/.version"
117+
118+
# 8. Write MANIFEST.json
119+
# Use sha256sum on Linux, shasum on macOS
120+
if command -v sha256sum >/dev/null 2>&1; then
121+
SHA_CMD="sha256sum"
122+
else
123+
SHA_CMD="shasum -a 256"
124+
fi
125+
CONTENTS_SHA=$(find "$TARBALL_ROOT" -type f -exec $SHA_CMD {} \; | sort | $SHA_CMD | cut -d' ' -f1)
126+
cat > "$TARBALL_ROOT/MANIFEST.json" <<MANIFEST
127+
{
128+
"version": "${VERSION}",
129+
"python_version": "${PYTHON_FULL_VERSION}",
130+
"uv_version": "${UV_VERSION}",
131+
"platform": "x86_64-unknown-linux-gnu",
132+
"sha256": "${CONTENTS_SHA}"
133+
}
134+
MANIFEST
135+
136+
# 9. Package tarball
137+
echo "Packaging tarball..."
138+
cd "$BUILD_DIR"
139+
tar czf "$OUTPUT_DIR/$TARBALL_NAME" flash-worker/
140+
141+
# 10. Report
142+
TARBALL_SIZE=$(du -h "$OUTPUT_DIR/$TARBALL_NAME" | cut -f1)
143+
echo ""
144+
echo "Tarball built: $OUTPUT_DIR/$TARBALL_NAME"
145+
echo "Size: $TARBALL_SIZE"
146+
echo "Version: $VERSION"
147+
echo "SHA256: $($SHA_CMD "$OUTPUT_DIR/$TARBALL_NAME" | cut -d' ' -f1)"
148+
149+
# Cleanup build dir (keep cache)
150+
rm -rf "$BUILD_DIR"

src/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,7 @@
5959
attempts to install it and retry. This caps the retry loop to prevent unbounded
6060
installs (e.g. a package with many missing transitive deps).
6161
"""
62+
63+
# Process Injection (Tarball Mode)
64+
FLASH_WORKER_INSTALL_DIR_ENV = "FLASH_WORKER_INSTALL_DIR"
65+
"""Env var set by bootstrap.sh to signal tarball mode. Value is the extraction directory."""

src/dependency_installer.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import List
66

77
from runpod_flash.protos.remote_execution import FunctionResponse
8-
from constants import LARGE_SYSTEM_PACKAGES, NAMESPACE
8+
from constants import FLASH_WORKER_INSTALL_DIR_ENV, LARGE_SYSTEM_PACKAGES, NAMESPACE
99
from subprocess_utils import run_logged_subprocess
1010

1111

@@ -16,6 +16,7 @@ def __init__(self):
1616
self.logger = logging.getLogger(f"{NAMESPACE}.{__name__.split('.')[-1]}")
1717
self._nala_available = None # Cache nala availability check
1818
self._is_docker = None # Cache Docker environment detection
19+
self._fw_install_dir = os.environ.get(FLASH_WORKER_INSTALL_DIR_ENV)
1920

2021
def install_dependencies(
2122
self, packages: List[str], accelerate_downloads: bool = True
@@ -35,7 +36,12 @@ def install_dependencies(
3536

3637
self.logger.info(f"Installing Python dependencies: {packages}")
3738

38-
if self._is_docker_environment():
39+
if self._fw_install_dir:
40+
# Tarball mode: install user deps into the flash-worker venv using bundled uv
41+
uv_bin = os.path.join(self._fw_install_dir, "uv")
42+
venv_python = os.path.join(self._fw_install_dir, "venv", "bin", "python")
43+
command = [uv_bin, "pip", "install", "--python", venv_python] + packages
44+
elif self._is_docker_environment():
3945
if accelerate_downloads:
4046
# Install into the running Python interpreter's environment.
4147
# Using --python sys.executable (not --system) ensures packages go into

0 commit comments

Comments
 (0)