Skip to content

Commit 1521206

Browse files
Merge pull request #8 from OlafenwaMoses/main
release: acceleration and publish pipeline
2 parents 93041ae + 7b4b67e commit 1521206

19 files changed

Lines changed: 283 additions & 92 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
python-version: "3.12"
4747

4848
- name: Install dependencies
49-
run: uv sync --frozen
49+
run: uv sync --frozen --extra cpu
5050

5151
- name: Run unit tests
5252
run: uv run pytest tests/unit/ -v
@@ -69,7 +69,7 @@ jobs:
6969
python-version: "3.12"
7070

7171
- name: Install dependencies
72-
run: uv sync --frozen
72+
run: uv sync --frozen --extra cpu
7373

7474
- name: Set model cache path
7575
run: echo "VIZION3D_MODEL_CACHE=$HOME/.cache/vizion3d/models" >> "$GITHUB_ENV"

.github/workflows/docs.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
name: Deploy docs
22

33
on:
4-
push:
5-
branches: [main]
6-
workflow_dispatch:
4+
workflow_run:
5+
workflows: ["Publish to PyPI"]
6+
types: [completed]
77

88
permissions:
99
contents: read
@@ -17,6 +17,7 @@ concurrency:
1717
jobs:
1818
build:
1919
runs-on: ubuntu-latest
20+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
2021
steps:
2122
- uses: actions/checkout@v4
2223

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Enforce release source branch
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- release
7+
8+
jobs:
9+
check-source:
10+
name: Source branch check
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Only allow PRs from main into release
14+
run: |
15+
if [ "${{ github.head_ref }}" != "main" ]; then
16+
echo "::error::Only PRs from 'main' are allowed into 'release' (got: '${{ github.head_ref }}')"
17+
exit 1
18+
fi

.github/workflows/publish.yml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: Publish to PyPI
2+
3+
# Manual trigger only — never runs automatically.
4+
# Must be dispatched from the 'release' branch via the GitHub Actions UI.
5+
on:
6+
workflow_dispatch:
7+
inputs:
8+
version:
9+
description: "Release version (e.g. 1.2.0)"
10+
required: true
11+
type: string
12+
13+
jobs:
14+
# ────────────────────────────────────────────────────────────────────────────
15+
# Guard — reject if triggered from any branch other than release
16+
# ────────────────────────────────────────────────────────────────────────────
17+
enforce-branch:
18+
name: Enforce release branch
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Must be triggered from the release branch
22+
run: |
23+
if [ "${{ github.ref }}" != "refs/heads/release" ]; then
24+
echo "::error::This workflow must be triggered from the 'release' branch (got: ${{ github.ref }})"
25+
exit 1
26+
fi
27+
28+
# ────────────────────────────────────────────────────────────────────────────
29+
# Publish — build and upload to PyPI
30+
# ────────────────────────────────────────────────────────────────────────────
31+
publish:
32+
name: Build and publish
33+
runs-on: ubuntu-latest
34+
needs: [enforce-branch]
35+
36+
steps:
37+
- uses: actions/checkout@v4
38+
39+
- uses: astral-sh/setup-uv@v5
40+
with:
41+
python-version: "3.12"
42+
43+
# ── Validate version format: must be major.minor.patch ───────────────
44+
- name: Validate version format
45+
run: |
46+
if ! echo "${{ github.event.inputs.version }}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
47+
echo "::error::Version '${{ github.event.inputs.version }}' is not valid. Must be major.minor.patch (e.g. 1.2.0)"
48+
exit 1
49+
fi
50+
echo "Version format is valid: ${{ github.event.inputs.version }}"
51+
52+
# ── Sanity check: reject if the requested version is not strictly
53+
# greater than the version currently live on PyPI. ──────────────────
54+
- name: Sanity-check version against PyPI
55+
run: |
56+
python3 - <<'PYEOF'
57+
import sys, json, urllib.request
58+
from packaging.version import Version
59+
60+
new_version = "${{ github.event.inputs.version }}"
61+
62+
try:
63+
with urllib.request.urlopen("https://pypi.org/pypi/vizion3d/json") as r:
64+
data = json.loads(r.read())
65+
current_version = data["info"]["version"]
66+
print(f"Current PyPI version : {current_version}")
67+
print(f"Requested version : {new_version}")
68+
if Version(new_version) <= Version(current_version):
69+
print(f"::error::Version {new_version} must be strictly greater than the current PyPI version {current_version}")
70+
sys.exit(1)
71+
print("Version check passed.")
72+
except urllib.error.HTTPError as e:
73+
if e.code == 404:
74+
print("Package not yet on PyPI — first publish, skipping version check.")
75+
else:
76+
raise
77+
PYEOF
78+
79+
- name: Set version in pyproject.toml
80+
run: |
81+
sed -i 's/^version = .*/version = "${{ github.event.inputs.version }}"/' pyproject.toml
82+
echo "Version set to ${{ github.event.inputs.version }}"
83+
84+
- name: Build package
85+
run: uv build
86+
87+
- name: Publish to PyPI
88+
run: uv publish --token ${{ secrets.PYPI_API_TOKEN }}

docs/features/depth_estimation.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ Depth estimation predicts the per-pixel distance from the camera for every pixel
1111

1212
| Value | What happens |
1313
|---|---|
14-
| `"depth-anything/Depth-Anything-V2-Base-hf"` *(default)* | Downloads the vizion3D release checkpoint (`depth_anything_v2_vitb.pth`) to `~/.cache/vizion3d/models/` on first use, then loads it directly |
15-
| Any other string | Passed through to Hugging Face `transformers.pipeline(task="depth-estimation", model=...)` |
14+
| *(default)* | Downloads the vizion3D release checkpoint (`depth_anything_v2_vitb.pth`) to `~/.cache/vizion3d/models/` on first use, then loads it directly |
15+
| An HTTPS URL ending in `.pth` or `.pt` | Downloaded to the cache directory on first use, then loaded as a Depth Anything V2 checkpoint |
1616
| A local `.pth` or `.pt` file path | Loaded directly as a Depth Anything V2 checkpoint — never downloaded |
1717

1818
Models are kept in memory after the first inference in the current process. Subsequent calls to any `DepthEstimation` instance reuse the loaded weights.
@@ -28,7 +28,7 @@ Set `VIZION3D_MODEL_CACHE` in your environment to change the default cache direc
2828
| Parameter | Type | Required | Default | Description |
2929
|---|---|---|---|---|
3030
| `image_input` | `str \| bytes` | **Yes** || Image to process. Pass a file path string or raw image bytes. |
31-
| `model_backend` | `str` | No | `"depth-anything/Depth-Anything-V2-Base-hf"` | Model backend identifier. See [Model backends](#model-backends) above. |
31+
| `model_backend` | `str` | No | vizion3D release checkpoint URL | Model backend identifier. See [Model backends](#model-backends) above. |
3232
| `return_depth_image` | `bool` | No | `False` | If `True`, the result includes a 16-bit grayscale Open3D Image of the depth map. |
3333
| `return_point_cloud` | `bool` | No | `False` | If `True`, the result includes an Open3D PointCloud unprojected from the RGB-D image. |
3434
| `return_mesh` | `bool` | No | `False` | If `True`, the result includes an Open3D TriangleMesh reconstructed from the point cloud via ball-pivoting. |
@@ -44,7 +44,7 @@ Set `VIZION3D_MODEL_CACHE` in your environment to change the default cache direc
4444
| `depth_map` | `list[list[float]]` | Yes | Raw floating-point depth array, shape `[H][W]`. Values are relative (not metric) — closer objects have higher values for inverse-depth models. |
4545
| `min_depth` | `float` | Yes | Minimum value in `depth_map`. |
4646
| `max_depth` | `float` | Yes | Maximum value in `depth_map`. Guaranteed `max_depth >= min_depth`. |
47-
| `backend_used` | `str` | Yes | Resolved model identifier that processed the request (local file path or HuggingFace model ID). |
47+
| `backend_used` | `str` | Yes | Resolved model identifier that processed the request (local file path). |
4848
| `depth_image` | `open3d.geometry.Image \| None` | When `return_depth_image=True` | 16-bit grayscale image, dtype `uint16`, shape `(H, W)`. The full 0–65535 range maps to `[min_depth, max_depth]`. |
4949
| `point_cloud` | `open3d.geometry.PointCloud \| None` | When `return_point_cloud=True` | Coloured 3D point cloud unprojected from the RGB-D image using PrimeSense default intrinsics. Coordinates are in metres. |
5050
| `mesh` | `open3d.geometry.TriangleMesh \| None` | When `return_mesh=True` | Triangle mesh surface reconstructed from the point cloud via ball-pivoting. Includes vertex colours. |
@@ -203,23 +203,23 @@ o3d.io.write_triangle_mesh("scene_mesh.ply", result.mesh)
203203

204204
## 7. Custom model backend
205205

206-
Use any Hugging Face depth estimation model or a local `.pth` checkpoint.
206+
Use a local `.pth` checkpoint or a remote URL to a `.pth` file.
207207

208208
```python
209209
from vizion3d.lifting import DepthEstimation, DepthEstimationCommand
210210

211-
# Hugging Face model
211+
# Local checkpoint
212212
cmd = DepthEstimationCommand(
213213
image_input="scene.png",
214-
model_backend="Intel/dpt-large",
214+
model_backend="/models/depth_anything_v2_vitl.pth",
215215
)
216216
result = DepthEstimation().run(cmd)
217217
print(f"Backend: {result.backend_used}")
218218

219-
# Local checkpoint
219+
# Remote checkpoint URL (downloaded and cached on first use)
220220
cmd = DepthEstimationCommand(
221221
image_input="scene.png",
222-
model_backend="/models/depth_anything_v2_vitl.pth",
222+
model_backend="https://example.com/weights/depth_anything_v2_vits.pth",
223223
)
224224
result = DepthEstimation().run(cmd)
225225
print(f"Backend: {result.backend_used}")

docs/index.md

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,136 @@ Every task is accessible through three consumption modes driven by one shared CQ
1616

1717
Requires **Python 3.12** (Open3D constraint).
1818

19+
PyTorch is **not bundled** in the base install — choose the extra that matches your hardware (see [Hardware acceleration](#hardware-acceleration) below). For most users the `cpu` extra is the right default; it installs the standard PyTorch wheel which covers CPU, NVIDIA CUDA, and Apple Silicon MPS automatically.
20+
1921
**pip**
20-
```bash
21-
pip install vizion3d
22+
```bash
23+
pip install "vizion3d[cpu]"
2224
```
2325

2426
**Poetry**
2527
```bash
26-
poetry add vizion3d
28+
poetry add "vizion3d[cpu]"
2729
```
2830

2931
**uv**
3032
```bash
3133
uv python pin 3.12
32-
uv add vizion3d
34+
uv add "vizion3d[cpu]"
35+
```
36+
37+
---
38+
39+
## Hardware acceleration
40+
41+
vizion3d detects the best available device automatically at runtime — no code changes required. Choose the install extra that matches your hardware.
42+
43+
| Backend | Hardware | Platforms | What drives inference |
44+
|---|---|---|---|
45+
| **CPU** | Any processor | Linux, Windows, macOS | PyTorch CPU kernels |
46+
| **CUDA** | NVIDIA GPU (Kepler+) | Linux, Windows | CUDA cores / Tensor Cores (Ampere+) |
47+
| **MPS** | Apple Silicon (M1 / M2 / M3 / M4) | macOS 12.3+ | Metal GPU via unified memory |
48+
| **ROCm** | AMD GPU (RDNA2+, CDNA, CDNA2, CDNA3) | **Linux only** | ROCm HIP runtime |
49+
50+
---
51+
52+
### CPU (default)
53+
54+
Works on every platform with no additional drivers. **This is the recommended install for most users.** Inference runs on PyTorch's CPU backend and automatically upgrades to NVIDIA CUDA or Apple Silicon MPS if detected at runtime — no separate install needed for those.
55+
56+
**pip**
57+
```bash
58+
pip install "vizion3d[cpu]"
59+
```
60+
61+
**uv**
62+
```bash
63+
uv add "vizion3d[cpu]"
64+
```
65+
66+
> **Mac M-series users:** the standard CPU extra automatically includes Metal Performance Shaders (MPS) support — no separate install needed. vizion3d will use your GPU via MPS as long as you are on macOS 12.3 or later with PyTorch ≥ 2.0.
67+
68+
---
69+
70+
### NVIDIA CUDA
71+
72+
Delivers the highest throughput for depth estimation. On NVIDIA Ampere GPUs and newer (RTX 30xx / A100 and above), PyTorch additionally uses Tensor Cores for mixed-precision acceleration.
73+
74+
#### Prerequisites
75+
76+
| Requirement | Minimum version | Link |
77+
|---|---|---|
78+
| NVIDIA GPU driver | 520.61.05 (Linux) / 528.33 (Windows) | [Driver downloads](https://www.nvidia.com/drivers) |
79+
| CUDA Toolkit | 11.8 | [CUDA Toolkit installer](https://developer.nvidia.com/cuda-downloads) |
80+
| cuDNN | 8.x | [cuDNN install guide](https://developer.nvidia.com/cudnn) |
81+
82+
Install CUDA and cuDNN **before** installing vizion3d. The PyTorch wheel bundled with the `cuda` extra already includes its own CUDA runtime libraries, but the driver must be present on the host.
83+
84+
**pip**
85+
```bash
86+
pip install "vizion3d[cuda]"
87+
```
88+
89+
**uv**
90+
```bash
91+
uv add "vizion3d[cuda]"
92+
```
93+
94+
vizion3d detects CUDA via `torch.cuda.is_available()` at runtime and moves models and tensors to the GPU automatically — no configuration needed.
95+
96+
---
97+
98+
### AMD ROCm
99+
100+
Provides GPU-accelerated inference on supported AMD GPUs using the ROCm open-source compute stack. ROCm exposes itself through PyTorch's CUDA namespace (`torch.cuda.is_available()` returns `True`), so vizion3d uses it transparently with no code changes.
101+
102+
> **Platform:** ROCm is supported on **Linux only**. There is no ROCm support for Windows or macOS.
103+
104+
#### Supported hardware
105+
106+
| Family | Examples |
107+
|---|---|
108+
| RDNA2 | RX 6700 XT, RX 6800, RX 6900 XT |
109+
| RDNA3 | RX 7800 XT, RX 7900 XTX |
110+
| CDNA | Instinct MI100 |
111+
| CDNA2 | Instinct MI200 series |
112+
| CDNA3 | Instinct MI300 series |
113+
114+
For the full supported GPU list see the [AMD ROCm hardware compatibility guide](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/reference/system-requirements.html).
115+
116+
#### Prerequisites
117+
118+
Install the ROCm stack on your system before installing the PyTorch ROCm wheel. Follow AMD's official guide:
119+
120+
- [ROCm installation for Linux](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/)
121+
122+
#### Install
123+
124+
Because the ROCm PyTorch wheel is hosted on PyTorch's own index (not PyPI), it must be installed **before** vizion3d — vizion3d's base install has no torch dependency and will not overwrite it.
125+
126+
**Step 1 — install ROCm PyTorch**
127+
```bash
128+
pip3 install --pre torch torchvision torchaudio \
129+
--index-url https://download.pytorch.org/whl/nightly/rocm7.2
130+
```
131+
132+
For the full list of available ROCm wheel versions see [PyTorch ROCm install guide](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/install/3rd-party/pytorch-install.html#using-wheels-package).
133+
134+
**Step 2 — install vizion3d (no extra needed)**
135+
```bash
136+
pip install vizion3d
33137
```
34138

139+
Because `vizion3d` declares no torch dependency in its base install, pip will not touch the ROCm wheel you installed in step 1.
140+
141+
> **Warning:** do **not** run `pip install "vizion3d[cpu]"` or `pip install "vizion3d[cuda]"` after installing the ROCm wheel — those extras pull a standard PyPI torch build and will replace your ROCm installation.
142+
143+
#### Limitations
144+
145+
- Linux only — ROCm does not run on Windows or macOS.
146+
- Only GPUs on AMD's official support list are guaranteed to work; consumer RDNA1 cards (RX 5000 series) are not supported.
147+
- Some PyTorch operations fall back to CPU on ROCm; performance for those ops will match CPU speed.
148+
35149
---
36150

37151
## Quick start — depth estimation

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,15 @@ dependencies = [
3535
"uvicorn>=0.23.0",
3636
"python-multipart>=0.0.6",
3737
"transformers>=5.6.2",
38-
"torch>=2.11.0",
39-
"torchvision>=0.26.0",
4038
"pillow>=12.2.0",
4139
"open3d>=0.18.0",
4240
]
4341

42+
[project.optional-dependencies]
43+
cpu = ["torch>=2.11.0", "torchvision>=0.26.0"]
44+
cuda = ["torch>=2.11.0", "torchvision>=0.26.0"]
45+
amd = []
46+
4447
[project.urls]
4548
Homepage = "https://github.com/OlafenwaMoses/vizion3D"
4649
Documentation = "https://olafenwamoses.github.io/vizion3D"

tests/integration/test_direct.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
pytest.importorskip("open3d", reason="open3d required — run: uv python pin 3.12 && uv sync")
2424

2525
from vizion3d.lifting import DepthEstimation, DepthEstimationCommand # noqa: E402
26-
from vizion3d.lifting.defaults import DEFAULT_DEPTH_MODEL_BACKEND # noqa: E402
26+
from vizion3d.lifting.defaults import DEFAULT_DEPTH_MODEL_URL # noqa: E402
2727
from vizion3d.lifting.handlers import DepthEstimationHandler # noqa: E402
2828
from vizion3d.lifting.utils import create_ply_binary # noqa: E402
2929

@@ -119,7 +119,7 @@ def _run_group(
119119
def test_direct_default_model(indoor_image_bytes, tmp_path, timing_collector):
120120
"""5 inferences with the default model backend (downloads to vizion3d cache)."""
121121
_run_group(
122-
model_backend=DEFAULT_DEPTH_MODEL_BACKEND,
122+
model_backend=DEFAULT_DEPTH_MODEL_URL,
123123
indoor_image_bytes=indoor_image_bytes,
124124
run_dir=tmp_path / "direct_default",
125125
entry_point="Direct",

tests/integration/test_grpc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
pytest.importorskip("open3d", reason="open3d required — run: uv python pin 3.12 && uv sync")
2222

23-
from vizion3d.lifting.defaults import DEFAULT_DEPTH_MODEL_BACKEND # noqa: E402
23+
from vizion3d.lifting.defaults import DEFAULT_DEPTH_MODEL_URL # noqa: E402
2424
from vizion3d.lifting.handlers import DepthEstimationHandler # noqa: E402
2525
from vizion3d.proto import lifting_pb2 # noqa: E402
2626

@@ -117,7 +117,7 @@ def test_grpc_default_model(
117117
):
118118
"""5 RPC calls with the default model backend."""
119119
_run_group(
120-
model_backend=DEFAULT_DEPTH_MODEL_BACKEND,
120+
model_backend=DEFAULT_DEPTH_MODEL_URL,
121121
indoor_image_bytes=indoor_image_bytes,
122122
run_dir=tmp_path / "grpc_default",
123123
scenario="Default model",

0 commit comments

Comments
 (0)