mirror of
https://github.com/elder-plinius/OBLITERATUS.git
synced 2026-08-29 22:20:36 +02:00
feat: add Jetson contributor validation path
This commit is contained in:
@@ -5,6 +5,11 @@ Date: 2026-08-21
|
||||
Issue: https://github.com/elder-plinius/OBLITERATUS/issues/31
|
||||
Public plan: [docs/platforms/jetson.md](../../../docs/platforms/jetson.md)
|
||||
|
||||
Implementation status: generic ARM64 preflight, vendor-PyTorch-preserving
|
||||
bootstrap, physical-device gate, sanitized evidence collector, manual runner
|
||||
workflow, and contributor issue form are implemented. The ADR remains Proposed
|
||||
until physical-device evidence fixes the initial supported matrix.
|
||||
|
||||
## Context
|
||||
|
||||
OBLITERATUS currently resolves CPU-only PyTorch for Linux through its default
|
||||
@@ -69,10 +74,12 @@ audited base-image and package provenance.
|
||||
1. Inventory the available Jetson and record its exact JetPack/L4T stack.
|
||||
2. Select and pin the initial Orin/JetPack 6.2.x matrix at an exact patch.
|
||||
3. Add generic ARM64 package/import preflight without changing default PR
|
||||
dependencies.
|
||||
4. Add the Jetson constraints/profile and preferred container recipe.
|
||||
dependencies. (Implemented.)
|
||||
4. Add the Jetson constraints/profile and preferred container recipe. (The
|
||||
native/container bootstrap boundary is implemented; an exact image remains
|
||||
dependent on the selected hardware matrix.)
|
||||
5. Add the `jetson-runtime` policy entry, test probe, trusted workflow job, and
|
||||
retained evidence artifact.
|
||||
retained evidence artifact. (Implemented.)
|
||||
6. Validate CUDA discovery, device selection, a tiny CUDA operation, and the
|
||||
offloaded-surgery probe. Validate source-built bitsandbytes separately.
|
||||
7. Claim support only for matrices with fresh green evidence on the exact
|
||||
|
||||
@@ -260,6 +260,11 @@ the Jetson compatibility matrix must be version-pinned, not “latest by default
|
||||
|
||||
## Recommended implementation path
|
||||
|
||||
Repository status as of 2026-08-21: steps 2 through 4 are implemented with a
|
||||
manual-only physical runner, a hosted ARM64 preflight, CPU-testable policy
|
||||
contracts, and a sanitized contributor evidence flow. Physical Jetson results
|
||||
are still required before completing steps 5 and 6 or publishing support.
|
||||
|
||||
1. Keep issue #31 open as a feature/specification item until the acceptance
|
||||
matrix is merged.
|
||||
2. Add a Jetson conditional policy entry and workflow job with an explicit
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Jetson runtime report
|
||||
description: Report a physical Jetson bootstrap, build, or CUDA validation result
|
||||
title: "[Jetson] "
|
||||
labels:
|
||||
- enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for testing OBLITERATUS on physical Jetson hardware. Run the
|
||||
documented `jetson-runtime` probe and attach its sanitized report. The
|
||||
report intentionally omits host identity, network, serial, token, and
|
||||
local-path data.
|
||||
- type: dropdown
|
||||
id: device
|
||||
attributes:
|
||||
label: Jetson device
|
||||
options:
|
||||
- Jetson AGX Orin
|
||||
- Jetson Orin NX
|
||||
- Jetson Orin Nano
|
||||
- Jetson AGX Xavier
|
||||
- Jetson AGX Thor
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: jetpack
|
||||
attributes:
|
||||
label: Exact JetPack version
|
||||
description: Include the patch version, for example 6.2.1.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: commit
|
||||
attributes:
|
||||
label: Exact OBLITERATUS commit
|
||||
description: Paste the 40-character commit SHA that was tested.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction commands
|
||||
description: Provide the smallest command sequence that demonstrates the result.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected result
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual result
|
||||
description: Include the complete error text, but remove any secrets before submitting.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: evidence
|
||||
attributes:
|
||||
label: Sanitized Jetson evidence
|
||||
description: Attach conditional-evidence/jetson-report.json or paste its JSON contents.
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: confirmations
|
||||
attributes:
|
||||
label: Confirmations
|
||||
options:
|
||||
- label: I ran this validation on physical Jetson hardware.
|
||||
required: true
|
||||
- label: I reviewed the report and removed any secrets or personal identifiers.
|
||||
required: true
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,6 +1,6 @@
|
||||
self-hosted-runner:
|
||||
# Project-owned capability labels used by conditional test runners.
|
||||
labels: [cuda, mps, mlx]
|
||||
labels: [cuda, jetson, mps, mlx]
|
||||
|
||||
# Configuration variables in array of strings defined in your repository or
|
||||
# organization. `null` means disabling configuration variables check.
|
||||
|
||||
@@ -225,6 +225,45 @@ jobs:
|
||||
scripts/check_supply_chain_policy.py
|
||||
scripts/gemma4_12b_recursive_loop.py || true
|
||||
|
||||
arm64-preflight:
|
||||
name: Linux ARM64 preflight
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
CUDA_VISIBLE_DEVICES: ""
|
||||
HF_DATASETS_OFFLINE: "1"
|
||||
HF_HUB_DISABLE_TELEMETRY: "1"
|
||||
HF_HUB_OFFLINE: "1"
|
||||
TRANSFORMERS_OFFLINE: "1"
|
||||
TEST_ENV: /tmp/obliteratus-arm64-preflight
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
- name: Install locked CPU runtime
|
||||
run: |
|
||||
python -m pip install "uv==${UV_VERSION}"
|
||||
UV_PROJECT_ENVIRONMENT="$TEST_ENV" \
|
||||
uv sync --locked --no-default-groups --extra dev --no-editable
|
||||
- name: Verify ARM64 package and core runtime
|
||||
run: |
|
||||
"$TEST_ENV/bin/python" -c \
|
||||
'import platform; assert platform.machine().lower() in {"aarch64", "arm64"}'
|
||||
"$TEST_ENV/bin/python" -m build --wheel
|
||||
"$TEST_ENV/bin/python" -c 'import obliteratus; print(obliteratus.__version__)'
|
||||
"$TEST_ENV/bin/python" -m obliteratus --help
|
||||
"$TEST_ENV/bin/python" -m pytest \
|
||||
tests/test_module_imports.py tests/test_device_boundaries.py \
|
||||
tests/test_jetson_support_tooling.py -q --no-cov
|
||||
|
||||
pr-core:
|
||||
name: Pull request core
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||
|
||||
@@ -23,6 +23,10 @@ on:
|
||||
description: Run CUDA and bitsandbytes on the labeled self-hosted runner
|
||||
type: boolean
|
||||
default: false
|
||||
run_jetson:
|
||||
description: Run Jetson CUDA on the trusted labeled physical runner
|
||||
type: boolean
|
||||
default: false
|
||||
run_mps:
|
||||
description: Run MPS on the labeled Apple Silicon runner
|
||||
type: boolean
|
||||
@@ -239,7 +243,7 @@ jobs:
|
||||
run: |
|
||||
python -m pip install "uv==${UV_VERSION}"
|
||||
UV_PROJECT_ENVIRONMENT="$CONDITIONAL_ENV" \
|
||||
uv sync --locked --no-default-groups --extra dev --no-editable
|
||||
uv sync --locked --no-default-groups --extra dev --extra quantization --no-editable
|
||||
CUDA_TORCH_VERSION="$("$CONDITIONAL_ENV/bin/python" -c \
|
||||
'import torch; print(torch.__version__.split("+", 1)[0])')"
|
||||
UV_TORCH_BACKEND=cu130 uv pip install \
|
||||
@@ -264,6 +268,52 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
jetson:
|
||||
name: NVIDIA Jetson runtime
|
||||
needs: policy
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.run_jetson
|
||||
runs-on: [self-hosted, linux, ARM64, jetson]
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
JETSON_ENV: /tmp/obliteratus-jetson-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
JETSON_TOOLS: /tmp/obliteratus-jetson-tools-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
steps:
|
||||
- name: Check out trusted candidate
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install pinned bootstrap tooling outside the JetPack runtime
|
||||
run: |
|
||||
python3 -m venv "$JETSON_TOOLS"
|
||||
"$JETSON_TOOLS/bin/python" -m pip install "uv==${UV_VERSION}"
|
||||
- name: Preserve JetPack PyTorch and install OBLITERATUS
|
||||
run: >-
|
||||
python3 scripts/setup_jetson.py
|
||||
--python python3
|
||||
--uv-python "$JETSON_TOOLS/bin/python"
|
||||
--venv "$JETSON_ENV"
|
||||
- name: Run physical Jetson CUDA probe
|
||||
run: >-
|
||||
"$JETSON_ENV/bin/python" scripts/run_conditional_gate.py jetson-runtime
|
||||
- name: Collect sanitized Jetson evidence
|
||||
if: always()
|
||||
run: |
|
||||
JETSON_PYTHON=python3
|
||||
if [ -x "$JETSON_ENV/bin/python" ]; then
|
||||
JETSON_PYTHON="$JETSON_ENV/bin/python"
|
||||
fi
|
||||
"$JETSON_PYTHON" scripts/jetson_support.py \
|
||||
--check \
|
||||
--gate-evidence conditional-evidence/jetson-runtime.json \
|
||||
--output conditional-evidence/jetson-report.json \
|
||||
--issue-body conditional-evidence/jetson-issue.md
|
||||
- name: Upload Jetson evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: conditional-jetson-${{ github.run_attempt }}
|
||||
path: conditional-evidence/
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
mps:
|
||||
name: Apple MPS runtime
|
||||
needs: policy
|
||||
@@ -393,20 +443,22 @@ jobs:
|
||||
summary:
|
||||
name: Conditional result and freshness summary
|
||||
if: always()
|
||||
needs: [policy, model_runtime, network_services, operator_ui, cuda, mps, mlx, remote]
|
||||
needs: [policy, model_runtime, network_services, operator_ui, cuda, jetson, mps, mlx, remote]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
CONDITIONAL_RESULTS: >-
|
||||
{"policy":"${{ needs.policy.result }}","model_runtime":"${{ needs.model_runtime.result }}",
|
||||
"network_services":"${{ needs.network_services.result }}","operator_ui":"${{ needs.operator_ui.result }}",
|
||||
"cuda":"${{ needs.cuda.result }}","mps":"${{ needs.mps.result }}","mlx":"${{ needs.mlx.result }}",
|
||||
"cuda":"${{ needs.cuda.result }}","jetson":"${{ needs.jetson.result }}",
|
||||
"mps":"${{ needs.mps.result }}","mlx":"${{ needs.mlx.result }}",
|
||||
"remote":"${{ needs.remote.result }}"}
|
||||
CONDITIONAL_SELECTED: >-
|
||||
{"model_runtime":${{ github.event_name != 'workflow_dispatch' || inputs.run_model }},
|
||||
"network_services":${{ github.event_name != 'workflow_dispatch' || inputs.run_network }},
|
||||
"operator_ui":${{ github.event_name != 'workflow_dispatch' || inputs.run_ui }},
|
||||
"cuda":${{ (github.event_name == 'workflow_dispatch' && inputs.run_cuda) || (github.event_name != 'workflow_dispatch' && vars.ENABLE_CUDA_GATE == 'true') }},
|
||||
"jetson":${{ github.event_name == 'workflow_dispatch' && inputs.run_jetson }},
|
||||
"mps":${{ (github.event_name == 'workflow_dispatch' && inputs.run_mps) || (github.event_name != 'workflow_dispatch' && vars.ENABLE_MPS_GATE == 'true') }},
|
||||
"mlx":${{ (github.event_name == 'workflow_dispatch' && inputs.run_mlx) || (github.event_name != 'workflow_dispatch' && vars.ENABLE_MLX_GATE == 'true') }},
|
||||
"remote":${{ (github.event_name == 'workflow_dispatch' && inputs.run_remote) || (github.event_name != 'workflow_dispatch' && vars.ENABLE_REMOTE_GATE == 'true') }}}
|
||||
|
||||
@@ -8,6 +8,7 @@ build/
|
||||
.eggs/
|
||||
*.egg
|
||||
.venv/
|
||||
.venv-jetson/
|
||||
venv/
|
||||
env/
|
||||
.env
|
||||
|
||||
+9
-2
@@ -206,8 +206,8 @@ them in the managed environment.
|
||||
|
||||
## Conditional and hardware testing
|
||||
|
||||
GPU, MPS, MLX, model-download, external-evaluation, network, operator-UI, and
|
||||
remote-execution checks are conditional release or risk-surface gates. A unit test
|
||||
GPU, Jetson, MPS, MLX, model-download, external-evaluation, network, operator-UI,
|
||||
and remote-execution checks are conditional release or risk-surface gates. A unit test
|
||||
with a mocked device is still required; hardware evidence complements deterministic
|
||||
contract coverage and never replaces it.
|
||||
|
||||
@@ -222,6 +222,13 @@ Current operator hardware includes Titan for CUDA/bitsandbytes probes and Mutsu,
|
||||
machines are attached to a public pull-request workflow; CI or a maintainer will
|
||||
record whether the mapped conditional gate ran.
|
||||
|
||||
Jetson contributors do not need project-owned hardware access. Follow the
|
||||
[Jetson contributor bootstrap](docs/platforms/jetson.md#experimental-contributor-bootstrap)
|
||||
on a physical device, then submit the generated sanitized evidence through the
|
||||
[Jetson runtime report](https://github.com/elder-plinius/OBLITERATUS/issues/new?template=jetson-runtime.yml). Maintainers
|
||||
will reproduce, add missing test depth, and integrate compatible changes. Never
|
||||
attach a contributor-controlled runner to untrusted pull-request execution.
|
||||
|
||||
## Security and supply-chain expectations
|
||||
|
||||
- Treat issue text, pull requests, patches, model repositories, checkpoints,
|
||||
|
||||
@@ -162,6 +162,11 @@ obliteratus ui --auth user:pass # add basic auth
|
||||
|
||||
The `obliteratus ui` command adds a Rich terminal startup with GPU detection and hardware-appropriate model recommendations. You can also run `python app.py` directly (same thing the Space uses).
|
||||
|
||||
Install `.[spaces,quantization]` instead when the UI must load supported
|
||||
bitsandbytes 8-bit or 4-bit models. Jetson users must follow the dedicated
|
||||
[Jetson bootstrap](docs/platforms/jetson.md); its bitsandbytes path is not yet
|
||||
supported.
|
||||
|
||||
### 3. Google Colab (free GPU)
|
||||
|
||||
[](https://colab.research.google.com/github/elder-plinius/OBLITERATUS/blob/main/notebooks/abliterate.ipynb)
|
||||
@@ -457,6 +462,12 @@ This sets `CUDA_VISIBLE_DEVICES` before CUDA initializes. The model is then shar
|
||||
|
||||
The `--dtype` flag controls the precision of model weights, which directly determines how much VRAM you need. Lower precision means smaller memory footprint at the cost of some numerical fidelity:
|
||||
|
||||
Install the optional backend before selecting a bitsandbytes mode:
|
||||
|
||||
```bash
|
||||
pip install -e ".[quantization]"
|
||||
```
|
||||
|
||||
| Dtype | Bytes/param | 7B model | 70B model | 405B model |
|
||||
|-------|-----------|---------|----------|-----------|
|
||||
| `float32` | 4 | 28 GB | 280 GB | 1620 GB |
|
||||
@@ -783,9 +794,10 @@ run the full Python matrix with 75% repository statement coverage, 60% repositor
|
||||
branch coverage, touched-module regression checks, mature-scope floors,
|
||||
deterministic property/order-repeat checks, selective mutation, package contracts,
|
||||
Windows portability, and supply-chain certification.
|
||||
Eight environment-bound test files run through the separately documented conditional
|
||||
workflow for model downloads, network services, operator UI, CUDA, bitsandbytes, MPS,
|
||||
MLX, and least-privileged remote execution.
|
||||
Nine environment-bound test files run through the separately documented conditional
|
||||
workflow for model downloads, network services, operator UI, CUDA, bitsandbytes,
|
||||
physical Jetson CUDA, MPS, MLX, and least-privileged remote execution. The Jetson
|
||||
lane is a trusted manual hardware probe; hosted ARM64 CI checks portability only.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -154,6 +154,19 @@
|
||||
"expected_cost": "included in cuda",
|
||||
"coverage_paths": ["obliteratus/models/loader.py"]
|
||||
},
|
||||
{
|
||||
"id": "jetson-runtime",
|
||||
"job": "jetson",
|
||||
"marker": "gpu",
|
||||
"runner": "self-hosted, linux, ARM64, jetson",
|
||||
"prerequisites": "trusted manual dispatch on physical Jetson hardware with a JetPack-aligned CUDA PyTorch runtime",
|
||||
"expected_cost": "under 30 self-hosted runner-minutes",
|
||||
"coverage_paths": [
|
||||
"obliteratus/device.py",
|
||||
"obliteratus/models/loader.py",
|
||||
"obliteratus/models/offload_surgery.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mps-runtime",
|
||||
"job": "mps",
|
||||
|
||||
@@ -13,13 +13,17 @@
|
||||
".github/workflows/**",
|
||||
"ci/**",
|
||||
"scripts/check_*.py",
|
||||
"scripts/jetson_*.py",
|
||||
"scripts/select_pr_tests.py",
|
||||
"scripts/setup_jetson.py",
|
||||
"pyproject.toml",
|
||||
"uv.lock"
|
||||
],
|
||||
"infrastructure_tests": [
|
||||
"tests/test_aiwg_workspace_contracts.py",
|
||||
"tests/test_ci_policy.py",
|
||||
"tests/test_conditional_gate_scripts.py",
|
||||
"tests/test_jetson_support_tooling.py",
|
||||
"tests/test_pr_test_selection.py",
|
||||
"tests/test_quality_policy.py",
|
||||
"tests/test_quality_gate_scripts.py",
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
"tests/test_offload_surgery.py",
|
||||
"tests/test_persistence_contracts.py",
|
||||
"tests/test_persistence_pipeline.py",
|
||||
"tests/conditional/test_cuda_runtime.py"
|
||||
"tests/conditional/test_cuda_runtime.py",
|
||||
"tests/conditional/test_jetson_runtime.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -134,7 +135,8 @@
|
||||
"tests/test_model_profile.py",
|
||||
"tests/test_model_profile_contracts.py",
|
||||
"tests/test_runtime_contracts.py",
|
||||
"tests/test_study_presets.py"
|
||||
"tests/test_study_presets.py",
|
||||
"tests/conditional/test_jetson_runtime.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -442,6 +444,7 @@
|
||||
],
|
||||
"conditional_gates": [
|
||||
"cuda-runtime",
|
||||
"jetson-runtime",
|
||||
"mps-runtime"
|
||||
]
|
||||
},
|
||||
@@ -466,6 +469,7 @@
|
||||
"conditional_gates": [
|
||||
"model-download-runtime",
|
||||
"cuda-runtime",
|
||||
"jetson-runtime",
|
||||
"bitsandbytes-runtime"
|
||||
]
|
||||
},
|
||||
@@ -489,7 +493,8 @@
|
||||
"tests/conditional/test_cuda_runtime.py"
|
||||
],
|
||||
"conditional_gates": [
|
||||
"cuda-runtime"
|
||||
"cuda-runtime",
|
||||
"jetson-runtime"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -70,21 +70,28 @@ probes.
|
||||
For an operator run on the labeled machine:
|
||||
|
||||
```bash
|
||||
uv sync --locked --extra dev
|
||||
uv sync --locked --extra dev --extra quantization
|
||||
CUDA_TORCH_VERSION="$(.venv/bin/python -c \
|
||||
'import torch; print(torch.__version__.split("+", 1)[0])')"
|
||||
UV_TORCH_BACKEND=cu130 uv pip install --python .venv/bin/python \
|
||||
--reinstall-package torch "torch==$CUDA_TORCH_VERSION"
|
||||
uv pip check --python .venv/bin/python
|
||||
uv run --extra dev python scripts/run_conditional_gate.py cuda-runtime
|
||||
uv run --extra dev python scripts/run_conditional_gate.py bitsandbytes-runtime
|
||||
uv run --extra dev --extra quantization python scripts/run_conditional_gate.py cuda-runtime
|
||||
uv run --extra dev --extra quantization python scripts/run_conditional_gate.py bitsandbytes-runtime
|
||||
```
|
||||
|
||||
Jetson CUDA support is tracked separately from this generic x64 CUDA lane. A
|
||||
generic Linux ARM build can prove package portability, but it does not prove
|
||||
Jetson GPU support because Jetson depends on a JetPack/L4T-matched CUDA, cuDNN,
|
||||
and PyTorch runtime. The support plan, recommended container path, and acceptance
|
||||
criteria are documented in [NVIDIA Jetson support plan](platforms/jetson.md).
|
||||
Jetson CUDA support is tracked separately from this generic x64 CUDA lane. The
|
||||
mandatory `Linux ARM64 preflight` uses GitHub's hosted `ubuntu-24.04-arm` runner
|
||||
to prove locked CPU packaging, imports, CLI startup, and Jetson tooling contracts.
|
||||
It is not GPU evidence. Jetson depends on a JetPack/L4T-matched CUDA, cuDNN, and
|
||||
PyTorch runtime.
|
||||
|
||||
Physical testing uses only a trusted manual dispatch on the labels `self-hosted`,
|
||||
`linux`, `ARM64`, and `jetson`. The job preserves NVIDIA's vendor PyTorch, runs
|
||||
`jetson-runtime`, and uploads the sanitized `conditional-jetson-<run-attempt>`
|
||||
artifact for 30 days. It never runs for a pull request, schedule, or release.
|
||||
Contributor bootstrap, runner isolation, reporting commands, and acceptance
|
||||
criteria are documented in the [NVIDIA Jetson support plan](platforms/jetson.md).
|
||||
|
||||
## Apple MPS and MLX
|
||||
|
||||
|
||||
@@ -79,6 +79,41 @@ from being selected: upstream documents that wheel as SBSA/server ARM and says
|
||||
Jetson L4T/JetPack requires a source build. Until a pinned source build passes
|
||||
on the selected device, bitsandbytes is unsupported for that tier.
|
||||
|
||||
### Experimental contributor bootstrap
|
||||
|
||||
Start with NVIDIA's PyTorch wheel or PyTorch iGPU container for the exact
|
||||
JetPack patch installed on the device. Confirm that `python3 -c 'import torch;
|
||||
assert torch.cuda.is_available()'` succeeds before installing OBLITERATUS. Then,
|
||||
from a checkout of the exact commit under test, run:
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv-jetson-tools
|
||||
.venv-jetson-tools/bin/python -m pip install "uv==0.12.4"
|
||||
.venv-jetson-tools/bin/python scripts/setup_jetson.py \
|
||||
--python python3 \
|
||||
--uv-python .venv-jetson-tools/bin/python \
|
||||
--venv .venv-jetson
|
||||
.venv-jetson/bin/python scripts/run_conditional_gate.py jetson-runtime
|
||||
.venv-jetson/bin/python scripts/jetson_support.py \
|
||||
--check \
|
||||
--gate-evidence conditional-evidence/jetson-runtime.json \
|
||||
--output conditional-evidence/jetson-report.json \
|
||||
--issue-body conditional-evidence/jetson-issue.md
|
||||
```
|
||||
|
||||
The bootstrap validates ARM64, L4T, and CUDA before changing the environment.
|
||||
It creates a virtual environment with `--system-site-packages`, exports the
|
||||
committed lock, and installs locked OBLITERATUS dependencies without replacing
|
||||
the vendor `torch`. It also excludes bitsandbytes. The generic bitsandbytes
|
||||
package is now an explicit `quantization` extra for supported non-Jetson
|
||||
environments; Jetson quantization remains a separate source-build milestone.
|
||||
|
||||
For JetPack 6.2, NVIDIA publishes the
|
||||
`nvcr.io/nvidia/pytorch:25.06-py3-igpu` container. Run it only on Jetson hardware
|
||||
with the NVIDIA runtime, mount a reviewed checkout, and use the same bootstrap
|
||||
inside the container. Match other JetPack releases through NVIDIA's
|
||||
compatibility table rather than substituting a `latest` tag.
|
||||
|
||||
## Conditional gate
|
||||
|
||||
Add a new gate instead of modifying the x64 CUDA gate:
|
||||
@@ -93,6 +128,35 @@ Add a new gate instead of modifying the x64 CUDA gate:
|
||||
The job must run only from a trusted ref or reviewed maintainer dispatch. A
|
||||
persistent self-hosted Jetson must never execute untrusted pull-request code.
|
||||
|
||||
### Attaching a contributor-owned runner
|
||||
|
||||
Register the runner using GitHub's self-hosted runner instructions, on the
|
||||
Jetson itself, and add the custom label `jetson`. GitHub supplies the
|
||||
`self-hosted`, `linux`, and `ARM64` default labels. Verify that the repository
|
||||
shows exactly these required labels before dispatching the job:
|
||||
|
||||
```text
|
||||
self-hosted, linux, ARM64, jetson
|
||||
```
|
||||
|
||||
Use a dedicated, non-personal runner account and a disposable or resettable
|
||||
workspace. Do not place Hugging Face, SSH, cloud, or signing credentials on the
|
||||
runner. Only a maintainer should manually dispatch `Conditional tests` against
|
||||
a reviewed commit; the Jetson job is deliberately unavailable to pull-request,
|
||||
scheduled, and release triggers. Remove the runner registration token after
|
||||
setup and keep the runner offline when it is not being used for reviewed work.
|
||||
|
||||
### Reporting results without a project-owned Jetson
|
||||
|
||||
Open the [Jetson runtime report](https://github.com/elder-plinius/OBLITERATUS/issues/new?template=jetson-runtime.yml)
|
||||
issue form and attach `conditional-evidence/jetson-report.json`, or paste the
|
||||
generated `conditional-evidence/jetson-issue.md`. The collector reports only an
|
||||
allow-listed architecture, OS/JetPack, PyTorch/CUDA, device-class, test-result,
|
||||
and commit profile. It excludes environment variables, hostnames, usernames,
|
||||
network addresses, device serials, tokens, and local filesystem paths. Review
|
||||
the file yourself before publishing it. A failed report is useful evidence and
|
||||
does not imply that the contributor must diagnose the compatibility problem.
|
||||
|
||||
The gate should verify:
|
||||
|
||||
- `platform.machine()` is `aarch64` or equivalent ARM64.
|
||||
@@ -146,3 +210,6 @@ them.
|
||||
- [Astral: Using uv with PyTorch](https://docs.astral.sh/uv/guides/integration/pytorch/)
|
||||
- [Hugging Face: bitsandbytes installation guide](https://huggingface.co/docs/bitsandbytes/installation)
|
||||
- [GitHub: secure use of self-hosted runners](https://docs.github.com/en/actions/reference/security/secure-use#hardening-for-self-hosted-runners)
|
||||
- [GitHub: use self-hosted runner labels](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/use-in-a-workflow)
|
||||
- [GitHub: hosted ARM64 runners](https://docs.github.com/en/actions/reference/runners/github-hosted-runners)
|
||||
- [NVIDIA: PyTorch 25.06 for JetPack 6.2](https://docs.nvidia.com/deeplearning/frameworks/pytorch-release-notes/rel-25-06.html)
|
||||
|
||||
+1
-1
@@ -34,7 +34,6 @@ dependencies = [
|
||||
"numpy>=1.24",
|
||||
"scikit-learn>=1.3",
|
||||
"tqdm>=4.64",
|
||||
"bitsandbytes>=0.46.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -51,6 +50,7 @@ dev = [
|
||||
"pytest-cov==7.1.0",
|
||||
"ruff==0.16.2",
|
||||
]
|
||||
quantization = ["bitsandbytes>=0.46.1"]
|
||||
spaces = ["gradio>=6.7,<7.0"]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Collect privacy-safe Jetson runtime evidence for CI and issue reports."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.metadata
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
ISSUE_URL = "https://github.com/elder-plinius/OBLITERATUS/issues/31"
|
||||
JETSON_RELEASE = Path("/etc/nv_tegra_release")
|
||||
OS_RELEASE = Path("/etc/os-release")
|
||||
SHA = re.compile(r"^[0-9a-f]{40}$")
|
||||
|
||||
|
||||
def _read_first_line(path: Path, *, limit: int = 500) -> str | None:
|
||||
try:
|
||||
return path.read_text(encoding="utf-8", errors="replace").splitlines()[0][:limit]
|
||||
except (OSError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def _os_release(path: Path) -> dict[str, str]:
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
except OSError:
|
||||
return {}
|
||||
values: dict[str, str] = {}
|
||||
for line in lines:
|
||||
key, separator, value = line.partition("=")
|
||||
if separator and key in {"ID", "VERSION_ID", "PRETTY_NAME"}:
|
||||
values[key.lower()] = value.strip().strip('"')[:200]
|
||||
return values
|
||||
|
||||
|
||||
def _capture(command: Sequence[str], *, timeout: int = 10) -> str | None:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
value = result.stdout.strip()
|
||||
return value[:500] or None
|
||||
|
||||
|
||||
def _candidate_sha() -> str:
|
||||
candidate = os.environ.get("GITHUB_SHA", "")
|
||||
if SHA.fullmatch(candidate):
|
||||
return candidate
|
||||
local = _capture(["git", "rev-parse", "HEAD"])
|
||||
return local if local is not None and SHA.fullmatch(local) else "local"
|
||||
|
||||
|
||||
def collect_host_facts(
|
||||
*,
|
||||
tegra_release: Path = JETSON_RELEASE,
|
||||
os_release: Path = OS_RELEASE,
|
||||
) -> dict[str, object]:
|
||||
"""Return an allow-listed host profile without identity or network data."""
|
||||
|
||||
return {
|
||||
"architecture": platform.machine(),
|
||||
"python_version": platform.python_version(),
|
||||
"os": _os_release(os_release),
|
||||
"l4t_release": _read_first_line(tegra_release),
|
||||
"jetpack_package": _capture(
|
||||
["dpkg-query", "-W", "-f=${Version}", "nvidia-jetpack"],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def collect_runtime_facts() -> dict[str, object]:
|
||||
"""Return an allow-listed PyTorch/GPU profile without serials or file paths."""
|
||||
|
||||
facts: dict[str, object] = {
|
||||
"torch_imported": False,
|
||||
"torch_version": None,
|
||||
"torch_cuda_version": None,
|
||||
"cuda_available": False,
|
||||
"cuda_device_count": 0,
|
||||
"device_name": None,
|
||||
"compute_capability": None,
|
||||
"total_memory_gb": None,
|
||||
"bitsandbytes_version": None,
|
||||
}
|
||||
try:
|
||||
import torch
|
||||
except Exception as exc: # pragma: no cover - exact vendor loader failures vary
|
||||
facts["torch_import_error"] = type(exc).__name__
|
||||
return facts
|
||||
|
||||
facts.update({
|
||||
"torch_imported": True,
|
||||
"torch_version": str(torch.__version__),
|
||||
"torch_cuda_version": torch.version.cuda,
|
||||
"cuda_available": bool(torch.cuda.is_available()),
|
||||
"cuda_device_count": int(torch.cuda.device_count()),
|
||||
})
|
||||
if facts["cuda_available"] and facts["cuda_device_count"]:
|
||||
properties = torch.cuda.get_device_properties(0)
|
||||
facts.update({
|
||||
"device_name": str(properties.name)[:200],
|
||||
"compute_capability": list(torch.cuda.get_device_capability(0)),
|
||||
"total_memory_gb": round(properties.total_memory / 1024 ** 3, 2),
|
||||
})
|
||||
try:
|
||||
facts["bitsandbytes_version"] = importlib.metadata.version("bitsandbytes")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
pass
|
||||
return facts
|
||||
|
||||
|
||||
def validate_report(report: dict[str, object]) -> tuple[list[str], list[str]]:
|
||||
"""Return blocking errors and non-blocking compatibility warnings."""
|
||||
|
||||
host = report["host"]
|
||||
runtime = report["runtime"]
|
||||
assert isinstance(host, dict)
|
||||
assert isinstance(runtime, dict)
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
if str(host.get("architecture", "")).lower() not in {"aarch64", "arm64"}:
|
||||
errors.append("host architecture is not ARM64")
|
||||
if not host.get("l4t_release"):
|
||||
errors.append("/etc/nv_tegra_release is unavailable; this is not a Jetson L4T runtime")
|
||||
if not runtime.get("torch_imported"):
|
||||
errors.append("PyTorch could not be imported from the JetPack-aligned runtime")
|
||||
elif not runtime.get("torch_cuda_version"):
|
||||
errors.append("PyTorch is not a CUDA build")
|
||||
elif not runtime.get("cuda_available"):
|
||||
errors.append("PyTorch cannot access the Jetson CUDA device")
|
||||
if runtime.get("bitsandbytes_version"):
|
||||
warnings.append(
|
||||
"bitsandbytes is installed but remains unsupported until its pinned Jetson "
|
||||
"source build passes the separate quantization probe",
|
||||
)
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def _gate_summary(path: Path | None) -> dict[str, object] | None:
|
||||
if path is None:
|
||||
return None
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {"status": "unavailable"}
|
||||
if not isinstance(value, dict):
|
||||
return {"status": "invalid"}
|
||||
summary: dict[str, object] = {}
|
||||
for key in ("gate", "status"):
|
||||
if isinstance(value.get(key), str):
|
||||
summary[key] = value[key][:100]
|
||||
git_sha = value.get("git_sha")
|
||||
if isinstance(git_sha, str) and (SHA.fullmatch(git_sha) or git_sha == "local"):
|
||||
summary["git_sha"] = git_sha
|
||||
counts = value.get("counts")
|
||||
if isinstance(counts, dict):
|
||||
summary["counts"] = {
|
||||
key: counts[key]
|
||||
for key in ("tests", "failures", "errors", "skipped")
|
||||
if isinstance(counts.get(key), int) and counts[key] >= 0
|
||||
}
|
||||
return summary
|
||||
|
||||
|
||||
def build_report(*, gate_evidence: Path | None = None) -> dict[str, object]:
|
||||
report: dict[str, object] = {
|
||||
"schema_version": 1,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"issue": ISSUE_URL,
|
||||
"git_sha": _candidate_sha(),
|
||||
"host": collect_host_facts(),
|
||||
"runtime": collect_runtime_facts(),
|
||||
}
|
||||
errors, warnings = validate_report(report)
|
||||
report["validation"] = {"errors": errors, "warnings": warnings}
|
||||
gate = _gate_summary(gate_evidence)
|
||||
if gate is not None:
|
||||
report["gate_evidence"] = gate
|
||||
return report
|
||||
|
||||
|
||||
def issue_body(report: dict[str, object]) -> str:
|
||||
return "\n".join([
|
||||
"## Jetson runtime report",
|
||||
"",
|
||||
"### What happened",
|
||||
"<!-- Describe the command, expected result, and actual result. -->",
|
||||
"",
|
||||
"### Reproduction",
|
||||
"<!-- Add the smallest command that reproduces the problem. -->",
|
||||
"",
|
||||
"### Sanitized environment evidence",
|
||||
"",
|
||||
"```json",
|
||||
json.dumps(report, indent=2, sort_keys=True),
|
||||
"```",
|
||||
"",
|
||||
"This report intentionally excludes environment variables, hostnames, usernames,",
|
||||
"network addresses, GPU serials, tokens, and local filesystem paths.",
|
||||
"",
|
||||
])
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
default=Path("conditional-evidence/jetson-report.json"),
|
||||
)
|
||||
parser.add_argument("--gate-evidence", type=Path)
|
||||
parser.add_argument("--issue-body", type=Path)
|
||||
parser.add_argument("--check", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
report = build_report(gate_evidence=args.gate_evidence)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
|
||||
if args.issue_body is not None:
|
||||
args.issue_body.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.issue_body.write_text(issue_body(report), encoding="utf-8")
|
||||
validation = report["validation"]
|
||||
assert isinstance(validation, dict)
|
||||
errors = validation["errors"]
|
||||
warnings = validation["warnings"]
|
||||
assert isinstance(errors, list)
|
||||
assert isinstance(warnings, list)
|
||||
for warning in warnings:
|
||||
print(f"WARNING: {warning}", file=sys.stderr)
|
||||
for error in errors:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
print(args.output)
|
||||
return 2 if args.check and errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -7,6 +7,7 @@ import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
@@ -21,24 +22,31 @@ GATES = {
|
||||
"operator-ui": "tests/conditional/test_operator_ui.py",
|
||||
"cuda-runtime": "tests/conditional/test_cuda_runtime.py",
|
||||
"bitsandbytes-runtime": "tests/conditional/test_cuda_runtime.py",
|
||||
"jetson-runtime": "tests/conditional/test_jetson_runtime.py",
|
||||
"mps-runtime": "tests/conditional/test_mps_runtime.py",
|
||||
"mlx-runtime": "tests/conditional/test_mlx_runtime.py",
|
||||
"remote-execution": "tests/conditional/test_remote_runtime.py",
|
||||
}
|
||||
JETSON_RELEASE = Path("/etc/nv_tegra_release")
|
||||
|
||||
|
||||
def missing_prerequisites(gate: str) -> list[str]:
|
||||
missing: list[str] = []
|
||||
if gate in {"cuda-runtime", "bitsandbytes-runtime", "mps-runtime"}:
|
||||
if gate in {"cuda-runtime", "bitsandbytes-runtime", "jetson-runtime", "mps-runtime"}:
|
||||
import torch
|
||||
|
||||
if gate.startswith("cuda") or gate.startswith("bitsandbytes"):
|
||||
if gate.startswith(("cuda", "bitsandbytes", "jetson")):
|
||||
if not torch.cuda.is_available():
|
||||
missing.append("a CUDA-capable PyTorch runtime")
|
||||
elif not (hasattr(torch.backends, "mps") and torch.backends.mps.is_available()):
|
||||
missing.append("an available Apple MPS backend")
|
||||
if gate == "bitsandbytes-runtime" and importlib.util.find_spec("bitsandbytes") is None:
|
||||
missing.append("bitsandbytes")
|
||||
if gate == "jetson-runtime":
|
||||
if platform.machine().lower() not in {"aarch64", "arm64"}:
|
||||
missing.append("an ARM64 host")
|
||||
if not JETSON_RELEASE.is_file():
|
||||
missing.append("a Jetson L4T runtime")
|
||||
if gate == "mlx-runtime":
|
||||
for module in ("mlx", "mlx_lm"):
|
||||
if importlib.util.find_spec(module) is None:
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create an OBLITERATUS venv without replacing JetPack's PyTorch runtime."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
EXCLUDED_PACKAGES = {"bitsandbytes", "torch"}
|
||||
NORMALIZE = re.compile(r"[-_.]+")
|
||||
|
||||
|
||||
def _run(command: Sequence[str], *, cwd: Path) -> None:
|
||||
print("+ " + " ".join(command))
|
||||
subprocess.run(command, cwd=cwd, check=True)
|
||||
|
||||
|
||||
def _require_new_or_reusable_venv(venv: Path, *, reuse: bool) -> None:
|
||||
if not venv.exists():
|
||||
return
|
||||
if not reuse:
|
||||
raise ValueError(f"virtual environment already exists: {venv}; pass --reuse to use it")
|
||||
config = venv / "pyvenv.cfg"
|
||||
try:
|
||||
contents = config.read_text(encoding="utf-8").lower()
|
||||
except OSError as exc:
|
||||
raise ValueError(f"existing path is not a reusable virtual environment: {venv}") from exc
|
||||
if "include-system-site-packages = true" not in contents:
|
||||
raise ValueError(f"existing virtual environment does not expose JetPack packages: {venv}")
|
||||
|
||||
|
||||
def _require_safe_target(venv: Path, project: Path) -> None:
|
||||
resolved = venv.resolve()
|
||||
forbidden = {Path("/").resolve(), Path.home().resolve(), project.resolve()}
|
||||
if resolved in forbidden:
|
||||
raise ValueError(f"refusing unsafe virtual environment target: {resolved}")
|
||||
|
||||
|
||||
def _require_exclusions(requirements: Path) -> None:
|
||||
emitted: set[str] = set()
|
||||
for raw_line in requirements.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith(("#", "--")):
|
||||
continue
|
||||
name = re.split(r"[<>=!~;@\[]", line, maxsplit=1)[0].strip()
|
||||
emitted.add(NORMALIZE.sub("-", name).lower())
|
||||
unexpected = sorted(EXCLUDED_PACKAGES & emitted)
|
||||
if unexpected:
|
||||
raise RuntimeError(f"Jetson export contains forbidden packages: {unexpected}")
|
||||
|
||||
|
||||
def prepare(
|
||||
*,
|
||||
project: Path,
|
||||
venv: Path,
|
||||
python: str,
|
||||
uv_python: str,
|
||||
reuse: bool,
|
||||
) -> None:
|
||||
project = project.resolve()
|
||||
support_script = project / "scripts" / "jetson_support.py"
|
||||
if not support_script.is_file() or not (project / "uv.lock").is_file():
|
||||
raise ValueError(f"not an OBLITERATUS checkout: {project}")
|
||||
_require_safe_target(venv, project)
|
||||
_require_new_or_reusable_venv(venv, reuse=reuse)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="obliteratus-jetson-") as temp_value:
|
||||
temp = Path(temp_value)
|
||||
_run(
|
||||
[python, str(support_script), "--check", "--output", str(temp / "host.json")],
|
||||
cwd=project,
|
||||
)
|
||||
_run([uv_python, "-m", "uv", "--version"], cwd=project)
|
||||
if not venv.exists():
|
||||
_run([python, "-m", "venv", "--system-site-packages", str(venv)], cwd=project)
|
||||
|
||||
target_python = venv / "bin" / "python"
|
||||
_run(
|
||||
[
|
||||
str(target_python),
|
||||
str(support_script),
|
||||
"--check",
|
||||
"--output",
|
||||
str(temp / "venv.json"),
|
||||
],
|
||||
cwd=project,
|
||||
)
|
||||
requirements = temp / "requirements-jetson.txt"
|
||||
_run(
|
||||
[
|
||||
uv_python,
|
||||
"-m",
|
||||
"uv",
|
||||
"export",
|
||||
"--locked",
|
||||
"--no-default-groups",
|
||||
"--extra",
|
||||
"dev",
|
||||
"--no-emit-project",
|
||||
"--no-emit-package",
|
||||
"torch",
|
||||
"--no-emit-package",
|
||||
"bitsandbytes",
|
||||
"--no-annotate",
|
||||
"--no-header",
|
||||
"--no-hashes",
|
||||
"--output-file",
|
||||
str(requirements),
|
||||
],
|
||||
cwd=project,
|
||||
)
|
||||
_require_exclusions(requirements)
|
||||
_run(
|
||||
[
|
||||
uv_python,
|
||||
"-m",
|
||||
"uv",
|
||||
"pip",
|
||||
"install",
|
||||
"--python",
|
||||
str(target_python),
|
||||
"--no-deps",
|
||||
"--requirements",
|
||||
str(requirements),
|
||||
],
|
||||
cwd=project,
|
||||
)
|
||||
_run(
|
||||
[
|
||||
uv_python,
|
||||
"-m",
|
||||
"uv",
|
||||
"pip",
|
||||
"install",
|
||||
"--python",
|
||||
str(target_python),
|
||||
"--no-deps",
|
||||
"--editable",
|
||||
str(project),
|
||||
],
|
||||
cwd=project,
|
||||
)
|
||||
_run(
|
||||
[uv_python, "-m", "uv", "pip", "check", "--python", str(target_python)],
|
||||
cwd=project,
|
||||
)
|
||||
|
||||
print("Jetson environment prepared without replacing vendor PyTorch.")
|
||||
print(f"Run: {target_python} scripts/run_conditional_gate.py jetson-runtime")
|
||||
print(
|
||||
f"Then: {target_python} scripts/jetson_support.py --check "
|
||||
"--gate-evidence conditional-evidence/jetson-runtime.json "
|
||||
"--issue-body conditional-evidence/jetson-issue.md",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--project", type=Path, default=Path(__file__).parents[1])
|
||||
parser.add_argument("--venv", type=Path, default=Path(".venv-jetson"))
|
||||
parser.add_argument("--python", default=sys.executable)
|
||||
parser.add_argument(
|
||||
"--uv-python",
|
||||
default=sys.executable,
|
||||
help="interpreter containing the pinned uv module (defaults to this interpreter)",
|
||||
)
|
||||
parser.add_argument("--reuse", action="store_true")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
prepare(
|
||||
project=args.project,
|
||||
venv=args.venv,
|
||||
python=args.python,
|
||||
uv_python=args.uv_python,
|
||||
reuse=args.reuse,
|
||||
)
|
||||
except (OSError, RuntimeError, subprocess.CalledProcessError, ValueError) as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Physical NVIDIA Jetson CUDA placement and operation probe."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from accelerate.hooks import AlignDevicesHook, add_hook_to_module
|
||||
|
||||
from obliteratus import device
|
||||
from obliteratus.abliterate import AbliterationPipeline
|
||||
|
||||
|
||||
pytestmark = pytest.mark.gpu
|
||||
|
||||
|
||||
def test_jetson_cuda_runtime_contract():
|
||||
assert platform.machine().lower() in {"aarch64", "arm64"}
|
||||
assert Path("/etc/nv_tegra_release").is_file()
|
||||
assert torch.version.cuda is not None
|
||||
assert torch.cuda.is_available()
|
||||
assert torch.cuda.device_count() > 0
|
||||
assert device.is_cuda()
|
||||
assert device.get_device("auto") == "cuda"
|
||||
tensor = torch.arange(16, device="cuda", dtype=torch.float32).reshape(4, 4)
|
||||
result = tensor @ tensor.T
|
||||
assert result.device.type == "cuda"
|
||||
assert torch.isfinite(result).all()
|
||||
|
||||
|
||||
def test_jetson_cuda_offloaded_surgery_contract():
|
||||
module = nn.Module()
|
||||
module.proj = nn.Linear(4, 4, bias=False)
|
||||
original = module.proj.weight.detach().clone()
|
||||
hook = AlignDevicesHook(execution_device="cuda", offload=True)
|
||||
add_hook_to_module(module.proj, hook)
|
||||
|
||||
count = AbliterationPipeline._project_out_advanced(
|
||||
module,
|
||||
torch.tensor([[1.0], [0.0], [0.0], [0.0]], device="cuda"),
|
||||
["proj"],
|
||||
)
|
||||
output = module.proj(torch.ones(1, 4, device="cuda"))
|
||||
|
||||
expected = original.clone()
|
||||
expected[:, 0] = 0
|
||||
assert count == 1
|
||||
assert output.device.type == "cuda"
|
||||
assert module.proj.weight.device.type == "meta"
|
||||
torch.testing.assert_close(hook.weights_map["weight"], expected)
|
||||
@@ -129,6 +129,24 @@ def test_pull_request_gate_is_fast_risk_mapped_and_uses_shared_floor():
|
||||
assert "tests/conditional/" in policy["excluded_test_prefixes"]
|
||||
|
||||
|
||||
def test_arm64_preflight_proves_portability_without_claiming_jetson_cuda():
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
arm = workflow.split(" arm64-preflight:\n", maxsplit=1)[1].split(
|
||||
" pr-core:\n",
|
||||
maxsplit=1,
|
||||
)[0]
|
||||
|
||||
assert "runs-on: ubuntu-24.04-arm" in arm
|
||||
assert "github.event_name == 'pull_request'" in arm
|
||||
assert "github.event_name == 'push' && github.ref == 'refs/heads/main'" in arm
|
||||
assert "-m build --wheel" in arm
|
||||
assert "import obliteratus" in arm
|
||||
assert "-m obliteratus --help" in arm
|
||||
assert "tests/test_device_boundaries.py" in arm
|
||||
assert "tests/test_jetson_support_tooling.py" in arm
|
||||
assert "jetson-runtime" not in arm
|
||||
|
||||
|
||||
def test_release_depth_jobs_only_run_for_tags_or_manual_validation():
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
release_condition = (
|
||||
|
||||
@@ -42,6 +42,7 @@ def test_cuda_job_replaces_locked_cpu_torch_with_same_version_cuda_build():
|
||||
cuda_job = workflow.split(" cuda:\n", maxsplit=1)[1].split(" mps:\n", maxsplit=1)[0]
|
||||
|
||||
assert "torch.__version__.split" in cuda_job
|
||||
assert "--extra quantization" in cuda_job
|
||||
assert "UV_TORCH_BACKEND=cu130 uv pip install" in cuda_job
|
||||
assert "--reinstall-package torch" in cuda_job
|
||||
assert '"torch==$CUDA_TORCH_VERSION"' in cuda_job
|
||||
@@ -49,6 +50,28 @@ def test_cuda_job_replaces_locked_cpu_torch_with_same_version_cuda_build():
|
||||
assert 'uv pip check --python "$CONDITIONAL_ENV/bin/python"' in cuda_job
|
||||
|
||||
|
||||
def test_jetson_job_is_manual_physical_trusted_and_retains_sanitized_evidence():
|
||||
workflow = (ROOT / ".github" / "workflows" / "conditional-tests.yml").read_text()
|
||||
jetson_job = workflow.split(" jetson:\n", maxsplit=1)[1].split(
|
||||
" mps:\n",
|
||||
maxsplit=1,
|
||||
)[0]
|
||||
|
||||
assert "github.event_name == 'workflow_dispatch' && inputs.run_jetson" in jetson_job
|
||||
assert "runs-on: [self-hosted, linux, ARM64, jetson]" in jetson_job
|
||||
assert "scripts/setup_jetson.py" in jetson_job
|
||||
assert "scripts/run_conditional_gate.py jetson-runtime" in jetson_job
|
||||
assert "scripts/jetson_support.py" in jetson_job
|
||||
assert "conditional-jetson-${{ github.run_attempt }}" in jetson_job
|
||||
assert "retention-days: 30" in jetson_job
|
||||
assert "actions/setup-python" not in jetson_job
|
||||
|
||||
policy = json.loads((ROOT / "ci" / "conditional-test-policy.json").read_text())
|
||||
gate = next(value for value in policy["gates"] if value["id"] == "jetson-runtime")
|
||||
assert gate["job"] == "jetson"
|
||||
assert gate["runner"] == "self-hosted, linux, ARM64, jetson"
|
||||
|
||||
|
||||
def test_policy_rejects_unknown_cpu_exclusion_gate(tmp_path):
|
||||
policy = json.loads((ROOT / "ci" / "conditional-test-policy.json").read_text())
|
||||
quality = {
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
"""CPU-testable contracts for the experimental Jetson support path."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.metadata
|
||||
import json
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from scripts import jetson_support
|
||||
from scripts import run_conditional_gate
|
||||
from scripts import setup_jetson
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
|
||||
|
||||
def test_host_evidence_is_allow_listed(monkeypatch, tmp_path):
|
||||
tegra = tmp_path / "nv_tegra_release"
|
||||
tegra.write_text("# R36 (release), REVISION: 4.3\nserial=secret\n")
|
||||
os_release = tmp_path / "os-release"
|
||||
os_release.write_text(
|
||||
'ID=ubuntu\nVERSION_ID="22.04"\nPRETTY_NAME="Ubuntu 22.04"\nSECRET=value\n'
|
||||
)
|
||||
monkeypatch.setattr(jetson_support.platform, "machine", lambda: "aarch64")
|
||||
monkeypatch.setattr(jetson_support.platform, "python_version", lambda: "3.10.12")
|
||||
monkeypatch.setattr(jetson_support, "_capture", lambda command, **kwargs: "6.2+b17")
|
||||
|
||||
facts = jetson_support.collect_host_facts(
|
||||
tegra_release=tegra,
|
||||
os_release=os_release,
|
||||
)
|
||||
|
||||
assert facts == {
|
||||
"architecture": "aarch64",
|
||||
"python_version": "3.10.12",
|
||||
"os": {
|
||||
"id": "ubuntu",
|
||||
"version_id": "22.04",
|
||||
"pretty_name": "Ubuntu 22.04",
|
||||
},
|
||||
"l4t_release": "# R36 (release), REVISION: 4.3",
|
||||
"jetpack_package": "6.2+b17",
|
||||
}
|
||||
assert "secret" not in json.dumps(facts).lower()
|
||||
|
||||
|
||||
def test_runtime_evidence_reports_cuda_without_device_identity(monkeypatch):
|
||||
properties = SimpleNamespace(name="Orin", total_memory=64 * 1024**3)
|
||||
fake_cuda = SimpleNamespace(
|
||||
is_available=lambda: True,
|
||||
device_count=lambda: 1,
|
||||
get_device_properties=lambda _index: properties,
|
||||
get_device_capability=lambda _index: (8, 7),
|
||||
)
|
||||
fake_torch = SimpleNamespace(
|
||||
__version__="2.8.0a0+nv25.06",
|
||||
version=SimpleNamespace(cuda="12.6"),
|
||||
cuda=fake_cuda,
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "torch", fake_torch)
|
||||
|
||||
def missing_package(_name):
|
||||
raise importlib.metadata.PackageNotFoundError
|
||||
|
||||
monkeypatch.setattr(jetson_support.importlib.metadata, "version", missing_package)
|
||||
|
||||
facts = jetson_support.collect_runtime_facts()
|
||||
|
||||
assert facts["cuda_available"] is True
|
||||
assert facts["device_name"] == "Orin"
|
||||
assert facts["compute_capability"] == [8, 7]
|
||||
assert facts["total_memory_gb"] == 64.0
|
||||
assert set(facts) == {
|
||||
"torch_imported",
|
||||
"torch_version",
|
||||
"torch_cuda_version",
|
||||
"cuda_available",
|
||||
"cuda_device_count",
|
||||
"device_name",
|
||||
"compute_capability",
|
||||
"total_memory_gb",
|
||||
"bitsandbytes_version",
|
||||
}
|
||||
|
||||
|
||||
def test_report_validation_blocks_non_jetson_or_non_cuda_and_warns_on_bnb():
|
||||
report = {
|
||||
"host": {"architecture": "x86_64", "l4t_release": None},
|
||||
"runtime": {
|
||||
"torch_imported": True,
|
||||
"torch_cuda_version": None,
|
||||
"cuda_available": False,
|
||||
"bitsandbytes_version": "0.47.0",
|
||||
},
|
||||
}
|
||||
|
||||
errors, warnings = jetson_support.validate_report(report)
|
||||
|
||||
assert len(errors) == 3
|
||||
assert any("ARM64" in error for error in errors)
|
||||
assert any("Jetson L4T" in error for error in errors)
|
||||
assert any("not a CUDA build" in error for error in errors)
|
||||
assert warnings and "unsupported" in warnings[0]
|
||||
|
||||
|
||||
def test_gate_evidence_and_issue_body_cannot_copy_arbitrary_fields(tmp_path):
|
||||
evidence = tmp_path / "gate.json"
|
||||
evidence.write_text(json.dumps({
|
||||
"gate": "jetson-runtime",
|
||||
"status": "passed",
|
||||
"git_sha": "a" * 40,
|
||||
"counts": {"tests": 1, "token": "nested-secret"},
|
||||
"token": "must-not-escape",
|
||||
"hostname": "must-not-escape",
|
||||
}))
|
||||
|
||||
summary = jetson_support._gate_summary(evidence)
|
||||
body = jetson_support.issue_body({"gate_evidence": summary})
|
||||
|
||||
assert summary == {
|
||||
"gate": "jetson-runtime",
|
||||
"status": "passed",
|
||||
"git_sha": "a" * 40,
|
||||
"counts": {"tests": 1},
|
||||
}
|
||||
assert "must-not-escape" not in body
|
||||
assert "nested-secret" not in body
|
||||
assert "excludes environment variables" in body
|
||||
|
||||
|
||||
def test_dependency_export_rejects_torch_and_bitsandbytes(tmp_path):
|
||||
requirements = tmp_path / "requirements.txt"
|
||||
requirements.write_text("transformers==4.56.0\npytest==8.4.1\n")
|
||||
setup_jetson._require_exclusions(requirements)
|
||||
|
||||
for forbidden in (
|
||||
"torch==2.8.0",
|
||||
"torch @ https://example.invalid/torch.whl",
|
||||
"bitsandbytes[diagnostics]==0.47.0",
|
||||
):
|
||||
requirements.write_text(forbidden + "\n")
|
||||
with pytest.raises(RuntimeError, match="forbidden packages"):
|
||||
setup_jetson._require_exclusions(requirements)
|
||||
|
||||
|
||||
def test_jetson_bootstrap_preserves_vendor_runtime_and_uses_locked_no_deps(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
):
|
||||
project = tmp_path / "project"
|
||||
(project / "scripts").mkdir(parents=True)
|
||||
(project / "scripts" / "jetson_support.py").write_text("# fixture\n")
|
||||
(project / "uv.lock").write_text("# fixture\n")
|
||||
venv = tmp_path / "jetson-venv"
|
||||
commands: list[list[str]] = []
|
||||
|
||||
def fake_run(command, *, cwd):
|
||||
command = list(command)
|
||||
commands.append(command)
|
||||
if command[1:4] == ["-m", "venv", "--system-site-packages"]:
|
||||
(venv / "bin").mkdir(parents=True)
|
||||
(venv / "pyvenv.cfg").write_text("include-system-site-packages = true\n")
|
||||
if "--output-file" in command:
|
||||
output = Path(command[command.index("--output-file") + 1])
|
||||
output.write_text("transformers==4.56.0\n")
|
||||
|
||||
monkeypatch.setattr(setup_jetson, "_run", fake_run)
|
||||
|
||||
setup_jetson.prepare(
|
||||
project=project,
|
||||
venv=venv,
|
||||
python="vendor-python",
|
||||
uv_python="tool-python",
|
||||
reuse=False,
|
||||
)
|
||||
|
||||
assert commands[0][0] == "vendor-python"
|
||||
assert "--check" in commands[0]
|
||||
export = next(command for command in commands if "export" in command)
|
||||
assert export.count("--no-emit-package") == 2
|
||||
assert "torch" in export and "bitsandbytes" in export
|
||||
installs = [command for command in commands if "install" in command]
|
||||
assert len(installs) == 2
|
||||
assert all("--no-deps" in command for command in installs)
|
||||
assert any("check" in command for command in commands)
|
||||
|
||||
|
||||
def test_bootstrap_rejects_unsafe_or_non_vendor_reusable_targets(tmp_path):
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
with pytest.raises(ValueError, match="unsafe"):
|
||||
setup_jetson._require_safe_target(project, project)
|
||||
|
||||
existing = tmp_path / "existing"
|
||||
existing.mkdir()
|
||||
(existing / "pyvenv.cfg").write_text("include-system-site-packages = false\n")
|
||||
with pytest.raises(ValueError, match="does not expose JetPack"):
|
||||
setup_jetson._require_new_or_reusable_venv(existing, reuse=True)
|
||||
|
||||
|
||||
def test_bitsandbytes_is_opt_in_for_quantization_only():
|
||||
metadata = tomllib.loads((ROOT / "pyproject.toml").read_text())
|
||||
base = metadata["project"]["dependencies"]
|
||||
extras = metadata["project"]["optional-dependencies"]
|
||||
|
||||
assert not any(value.startswith("bitsandbytes") for value in base)
|
||||
assert extras["quantization"] == ["bitsandbytes>=0.46.1"]
|
||||
|
||||
|
||||
def test_jetson_issue_form_requires_reproducible_sanitized_hardware_evidence():
|
||||
form = yaml.safe_load(
|
||||
(ROOT / ".github" / "ISSUE_TEMPLATE" / "jetson-runtime.yml").read_text(),
|
||||
)
|
||||
fields = {value.get("id"): value for value in form["body"] if value.get("id")}
|
||||
|
||||
assert set(fields) == {
|
||||
"device",
|
||||
"jetpack",
|
||||
"commit",
|
||||
"reproduction",
|
||||
"expected",
|
||||
"actual",
|
||||
"evidence",
|
||||
"confirmations",
|
||||
}
|
||||
assert all(value.get("validations", {}).get("required") for value in fields.values())
|
||||
confirmations = fields["confirmations"]["attributes"]["options"]
|
||||
assert all(option["required"] for option in confirmations)
|
||||
assert any("secrets" in option["label"] for option in confirmations)
|
||||
|
||||
|
||||
def test_jetson_conditional_prerequisites_are_physical_and_cuda(monkeypatch, tmp_path):
|
||||
fake_torch = SimpleNamespace(cuda=SimpleNamespace(is_available=lambda: True))
|
||||
monkeypatch.setitem(sys.modules, "torch", fake_torch)
|
||||
monkeypatch.setattr(run_conditional_gate.platform, "machine", lambda: "aarch64")
|
||||
tegra = tmp_path / "nv_tegra_release"
|
||||
tegra.write_text("# R36\n")
|
||||
monkeypatch.setattr(run_conditional_gate, "JETSON_RELEASE", tegra)
|
||||
assert run_conditional_gate.missing_prerequisites("jetson-runtime") == []
|
||||
|
||||
monkeypatch.setattr(run_conditional_gate.platform, "machine", lambda: "x86_64")
|
||||
fake_torch.cuda.is_available = lambda: False
|
||||
tegra.unlink()
|
||||
assert run_conditional_gate.missing_prerequisites("jetson-runtime") == [
|
||||
"a CUDA-capable PyTorch runtime",
|
||||
"an ARM64 host",
|
||||
"a Jetson L4T runtime",
|
||||
]
|
||||
@@ -15,7 +15,7 @@ resolution-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-08-11T18:17:23.461940614Z"
|
||||
exclude-newer = "2026-08-19T00:05:48.361273363Z"
|
||||
exclude-newer-span = "P3D"
|
||||
|
||||
[manifest]
|
||||
@@ -2602,7 +2602,6 @@ version = "0.1.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "accelerate" },
|
||||
{ name = "bitsandbytes" },
|
||||
{ name = "datasets" },
|
||||
{ name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "matplotlib", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
@@ -2631,6 +2630,9 @@ dev = [
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
quantization = [
|
||||
{ name = "bitsandbytes" },
|
||||
]
|
||||
spaces = [
|
||||
{ name = "gradio" },
|
||||
]
|
||||
@@ -2652,7 +2654,7 @@ quality = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "accelerate", specifier = ">=0.24" },
|
||||
{ name = "bitsandbytes", specifier = ">=0.46.1" },
|
||||
{ name = "bitsandbytes", marker = "extra == 'quantization'", specifier = ">=0.46.1" },
|
||||
{ name = "build", marker = "extra == 'dev'", specifier = "==1.2.2.post1" },
|
||||
{ name = "datasets", specifier = ">=2.14" },
|
||||
{ name = "gradio", marker = "extra == 'spaces'", specifier = ">=6.7,<7.0" },
|
||||
@@ -2674,7 +2676,7 @@ requires-dist = [
|
||||
{ name = "tqdm", specifier = ">=4.64" },
|
||||
{ name = "transformers", specifier = ">=4.40" },
|
||||
]
|
||||
provides-extras = ["dev", "spaces"]
|
||||
provides-extras = ["dev", "quantization", "spaces"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
ci = [
|
||||
|
||||
Reference in New Issue
Block a user