hyperstudy-perception 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hyperstudy_perception-0.1.0/LICENSE +21 -0
- hyperstudy_perception-0.1.0/PKG-INFO +53 -0
- hyperstudy_perception-0.1.0/README.md +19 -0
- hyperstudy_perception-0.1.0/pyproject.toml +64 -0
- hyperstudy_perception-0.1.0/src/perception/__init__.py +0 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/RELEASE.json +59 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/cloud/runpod/guard.sh +37 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/cloud/runpod/image/Dockerfile +39 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/cloud/runpod/image/start.sh +11 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/cloud/runpod/image/tag.py +40 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/cloud/skypilot/gpu-faces.yaml +39 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/cloud/skypilot/gpu-tune.yaml +55 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/cloud/skypilot/guard.sh +20 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/cloud/skypilot/iam-perception-node.json +18 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/cloud/skypilot/launch.sh +63 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/cloud/skypilot/node-gpu.yaml +25 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/cloud/skypilot/node-setup.sh +22 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/cloud/skypilot/sky.yaml +7 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/cloud/skypilot/smoke.yaml +28 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/envs/api/pyproject.toml +12 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/envs/api/uv.lock +276 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/envs/audio/pyproject.toml +12 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/envs/audio/uv.lock +575 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/envs/core/pyproject.toml +12 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/envs/core/uv.lock +157 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/envs/faces/pyproject.toml +12 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/envs/faces/uv.lock +1180 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/graphs/decode.yaml +11 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/graphs/faces.yaml +14 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/graphs/full.yaml +31 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/graphs/mvp-captions.yaml +27 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/graphs/mvp.yaml +17 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/audio/asr/manifest.yaml +20 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/audio/asr/node.py +74 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/audio/asr/prepare.py +12 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/audio/asr/test_wav.py +38 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/audio/asr/wav.py +19 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/conftest.py +64 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/core/decode/manifest.yaml +19 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/core/decode/media.py +104 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/core/decode/node.py +56 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/core/decode/test_media.py +65 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/testdata/README.md +10 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/testdata/rotated-90.mp4 +0 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/vision/face-track/frames.py +88 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/vision/face-track/loader.py +18 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/vision/face-track/manifest.yaml +30 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/vision/face-track/node.py +117 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/vision/face-track/prepare.py +8 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/vision/face-track/test_frames.py +46 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/vision/face-track/test_loader.py +16 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/vision/face-track/test_thumbnails.py +66 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/vision/face-track/thumbnails.py +68 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/vision/face-track/tracking.py +150 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/vlm/shot-captions/manifest.yaml +29 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/nodes/vlm/shot-captions/node.py +185 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/schema/README.md +54 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/schema/bundle-0.1.schema.json +371 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/sdk/pyproject.toml +13 -0
- hyperstudy_perception-0.1.0/src/perception/_pipeline/sdk/src/perception_node/__init__.py +157 -0
- hyperstudy_perception-0.1.0/src/perception/artifacts.py +180 -0
- hyperstudy_perception-0.1.0/src/perception/backends/__init__.py +0 -0
- hyperstudy_perception-0.1.0/src/perception/backends/base.py +179 -0
- hyperstudy_perception-0.1.0/src/perception/backends/cloud.py +214 -0
- hyperstudy_perception-0.1.0/src/perception/backends/native.py +140 -0
- hyperstudy_perception-0.1.0/src/perception/backends/probe.py +44 -0
- hyperstudy_perception-0.1.0/src/perception/backends/remote.py +140 -0
- hyperstudy_perception-0.1.0/src/perception/cli.py +780 -0
- hyperstudy_perception-0.1.0/src/perception/estimates.py +68 -0
- hyperstudy_perception-0.1.0/src/perception/export.py +100 -0
- hyperstudy_perception-0.1.0/src/perception/hosts.py +201 -0
- hyperstudy_perception-0.1.0/src/perception/ir.py +245 -0
- hyperstudy_perception-0.1.0/src/perception/keys.py +259 -0
- hyperstudy_perception-0.1.0/src/perception/manifests.py +169 -0
- hyperstudy_perception-0.1.0/src/perception/plan.py +170 -0
- hyperstudy_perception-0.1.0/src/perception/release.py +61 -0
- hyperstudy_perception-0.1.0/src/perception/remote_driver.py +990 -0
- hyperstudy_perception-0.1.0/src/perception/runpod.py +100 -0
- hyperstudy_perception-0.1.0/src/perception/scheduler.py +552 -0
- hyperstudy_perception-0.1.0/src/perception/setup.py +72 -0
- hyperstudy_perception-0.1.0/src/perception/store.py +219 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luke Chang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hyperstudy-perception
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Video perception pipeline: shots, speech, faces and captions from a video, as a versioned track bundle, run locally, on cloud GPUs or lab servers
|
|
5
|
+
Project-URL: Homepage, https://github.com/hyperstudyio/hyperstudy-perception
|
|
6
|
+
Project-URL: Changelog, https://github.com/hyperstudyio/hyperstudy-perception/releases
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: annotated-types==0.8.0
|
|
11
|
+
Requires-Dist: attrs==26.1.0
|
|
12
|
+
Requires-Dist: av==16.1.0
|
|
13
|
+
Requires-Dist: cffi==2.1.1; platform_python_implementation != 'PyPy' and sys_platform == 'linux'
|
|
14
|
+
Requires-Dist: cryptography==50.0.2; sys_platform == 'linux'
|
|
15
|
+
Requires-Dist: jaraco-classes==3.4.0
|
|
16
|
+
Requires-Dist: jaraco-context==6.1.2
|
|
17
|
+
Requires-Dist: jaraco-functools==4.6.0
|
|
18
|
+
Requires-Dist: jeepney==0.9.0; sys_platform == 'linux'
|
|
19
|
+
Requires-Dist: jsonschema-specifications==2025.9.1
|
|
20
|
+
Requires-Dist: jsonschema==4.26.0
|
|
21
|
+
Requires-Dist: keyring==25.7.0
|
|
22
|
+
Requires-Dist: more-itertools==11.2.1
|
|
23
|
+
Requires-Dist: pycparser==3.12; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'
|
|
24
|
+
Requires-Dist: pydantic-core==2.50.1
|
|
25
|
+
Requires-Dist: pydantic==2.14.1
|
|
26
|
+
Requires-Dist: pywin32-ctypes==0.2.3; sys_platform == 'win32'
|
|
27
|
+
Requires-Dist: pyyaml==6.0.3
|
|
28
|
+
Requires-Dist: referencing==0.37.0
|
|
29
|
+
Requires-Dist: rpds-py==2026.9.1
|
|
30
|
+
Requires-Dist: secretstorage==3.5.0; sys_platform == 'linux'
|
|
31
|
+
Requires-Dist: typing-extensions==4.16.0
|
|
32
|
+
Requires-Dist: typing-inspection==0.4.4
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# hyperstudy-perception
|
|
36
|
+
|
|
37
|
+
Turn a video into a time-indexed **perception track**: shots and keyframes, a word-timed transcript, face tracks
|
|
38
|
+
clustered into people, and shot captions. The result is a versioned track bundle that the
|
|
39
|
+
[HyperStudy Annotator](https://github.com/hyperstudyio/hyperstudy-annotator) imports for review and correction.
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
uv tool install hyperstudy-perception
|
|
43
|
+
perception doctor # what can run on this machine
|
|
44
|
+
perception graphs # the graphs it can run, by name
|
|
45
|
+
perception run full.yaml --input video=clip.mp4 --only asr,faces --allow-downloads
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Each step runs in its own locked environment, made by [uv](https://docs.astral.sh/uv/) the first time it is needed
|
|
49
|
+
(`perception setup` makes them all ahead of time), under `~/.hyperstudy-perception`. Nothing else is needed on the
|
|
50
|
+
machine: video is read with PyAV, whose wheels carry FFmpeg.
|
|
51
|
+
|
|
52
|
+
Model weights are downloaded at run time under their own licences; several (py-feat, ArcFace) are non-commercial, and
|
|
53
|
+
each bundle records the most restrictive licence among its layers.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# hyperstudy-perception
|
|
2
|
+
|
|
3
|
+
Turn a video into a time-indexed **perception track**: shots and keyframes, a word-timed transcript, face tracks
|
|
4
|
+
clustered into people, and shot captions. The result is a versioned track bundle that the
|
|
5
|
+
[HyperStudy Annotator](https://github.com/hyperstudyio/hyperstudy-annotator) imports for review and correction.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
uv tool install hyperstudy-perception
|
|
9
|
+
perception doctor # what can run on this machine
|
|
10
|
+
perception graphs # the graphs it can run, by name
|
|
11
|
+
perception run full.yaml --input video=clip.mp4 --only asr,faces --allow-downloads
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Each step runs in its own locked environment, made by [uv](https://docs.astral.sh/uv/) the first time it is needed
|
|
15
|
+
(`perception setup` makes them all ahead of time), under `~/.hyperstudy-perception`. Nothing else is needed on the
|
|
16
|
+
machine: video is read with PyAV, whose wheels carry FFmpeg.
|
|
17
|
+
|
|
18
|
+
Model weights are downloaded at run time under their own licences; several (py-feat, ArcFace) are non-commercial, and
|
|
19
|
+
each bundle records the most restrictive licence among its layers.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hyperstudy-perception"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Video perception pipeline: shots, speech, faces and captions from a video, as a versioned track bundle, run locally, on cloud GPUs or lab servers"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"] # the repository's, copied in by scripts/build-dist.sh
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"annotated-types==0.8.0",
|
|
11
|
+
"attrs==26.1.0",
|
|
12
|
+
"av==16.1.0",
|
|
13
|
+
"cffi==2.1.1 ; platform_python_implementation != 'PyPy' and sys_platform == 'linux'",
|
|
14
|
+
"cryptography==50.0.2 ; sys_platform == 'linux'",
|
|
15
|
+
"jaraco-classes==3.4.0",
|
|
16
|
+
"jaraco-context==6.1.2",
|
|
17
|
+
"jaraco-functools==4.6.0",
|
|
18
|
+
"jeepney==0.9.0 ; sys_platform == 'linux'",
|
|
19
|
+
"jsonschema==4.26.0",
|
|
20
|
+
"jsonschema-specifications==2025.9.1",
|
|
21
|
+
"keyring==25.7.0",
|
|
22
|
+
"more-itertools==11.2.1",
|
|
23
|
+
"pycparser==3.12 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'",
|
|
24
|
+
"pydantic==2.14.1",
|
|
25
|
+
"pydantic-core==2.50.1",
|
|
26
|
+
"pywin32-ctypes==0.2.3 ; sys_platform == 'win32'",
|
|
27
|
+
"pyyaml==6.0.3",
|
|
28
|
+
"referencing==0.37.0",
|
|
29
|
+
"rpds-py==2026.9.1",
|
|
30
|
+
"secretstorage==3.5.0 ; sys_platform == 'linux'",
|
|
31
|
+
"typing-extensions==4.16.0",
|
|
32
|
+
"typing-inspection==0.4.4",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/hyperstudyio/hyperstudy-perception"
|
|
37
|
+
Changelog = "https://github.com/hyperstudyio/hyperstudy-perception/releases"
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
perception = "perception.cli:main"
|
|
41
|
+
|
|
42
|
+
[tool.uv.sources]
|
|
43
|
+
perception-node = { path = "../sdk", editable = true }
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
# the node SDK only for the tests' stand-in nodes, which run in this interpreter: real nodes get it in their own
|
|
47
|
+
# environments (envs/*), from the copy the package carries
|
|
48
|
+
dev = ["pytest>=8", "perception-node"]
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["hatchling"]
|
|
52
|
+
build-backend = "hatchling.build"
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["src/perception"]
|
|
56
|
+
# the pipeline's files the package carries (scripts/build-dist.sh puts them there; ignored by git, so named here)
|
|
57
|
+
artifacts = ["src/perception/_pipeline/**"]
|
|
58
|
+
|
|
59
|
+
[tool.hatch.build.targets.sdist]
|
|
60
|
+
include = ["src/perception", "README.md"]
|
|
61
|
+
artifacts = ["src/perception/_pipeline/**"]
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
testpaths = ["tests"]
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"commit": "9beebed7f711b6c85e2a735810db16841a741f39",
|
|
3
|
+
"files": [
|
|
4
|
+
"cloud/runpod/guard.sh",
|
|
5
|
+
"cloud/runpod/image/Dockerfile",
|
|
6
|
+
"cloud/runpod/image/start.sh",
|
|
7
|
+
"cloud/runpod/image/tag.py",
|
|
8
|
+
"cloud/skypilot/gpu-faces.yaml",
|
|
9
|
+
"cloud/skypilot/gpu-tune.yaml",
|
|
10
|
+
"cloud/skypilot/guard.sh",
|
|
11
|
+
"cloud/skypilot/iam-perception-node.json",
|
|
12
|
+
"cloud/skypilot/launch.sh",
|
|
13
|
+
"cloud/skypilot/node-gpu.yaml",
|
|
14
|
+
"cloud/skypilot/node-setup.sh",
|
|
15
|
+
"cloud/skypilot/sky.yaml",
|
|
16
|
+
"cloud/skypilot/smoke.yaml",
|
|
17
|
+
"envs/api/pyproject.toml",
|
|
18
|
+
"envs/api/uv.lock",
|
|
19
|
+
"envs/audio/pyproject.toml",
|
|
20
|
+
"envs/audio/uv.lock",
|
|
21
|
+
"envs/core/pyproject.toml",
|
|
22
|
+
"envs/core/uv.lock",
|
|
23
|
+
"envs/faces/pyproject.toml",
|
|
24
|
+
"envs/faces/uv.lock",
|
|
25
|
+
"graphs/decode.yaml",
|
|
26
|
+
"graphs/faces.yaml",
|
|
27
|
+
"graphs/full.yaml",
|
|
28
|
+
"graphs/mvp-captions.yaml",
|
|
29
|
+
"graphs/mvp.yaml",
|
|
30
|
+
"nodes/audio/asr/manifest.yaml",
|
|
31
|
+
"nodes/audio/asr/node.py",
|
|
32
|
+
"nodes/audio/asr/prepare.py",
|
|
33
|
+
"nodes/audio/asr/test_wav.py",
|
|
34
|
+
"nodes/audio/asr/wav.py",
|
|
35
|
+
"nodes/conftest.py",
|
|
36
|
+
"nodes/core/decode/manifest.yaml",
|
|
37
|
+
"nodes/core/decode/media.py",
|
|
38
|
+
"nodes/core/decode/node.py",
|
|
39
|
+
"nodes/core/decode/test_media.py",
|
|
40
|
+
"nodes/testdata/README.md",
|
|
41
|
+
"nodes/testdata/rotated-90.mp4",
|
|
42
|
+
"nodes/vision/face-track/frames.py",
|
|
43
|
+
"nodes/vision/face-track/loader.py",
|
|
44
|
+
"nodes/vision/face-track/manifest.yaml",
|
|
45
|
+
"nodes/vision/face-track/node.py",
|
|
46
|
+
"nodes/vision/face-track/prepare.py",
|
|
47
|
+
"nodes/vision/face-track/test_frames.py",
|
|
48
|
+
"nodes/vision/face-track/test_loader.py",
|
|
49
|
+
"nodes/vision/face-track/test_thumbnails.py",
|
|
50
|
+
"nodes/vision/face-track/thumbnails.py",
|
|
51
|
+
"nodes/vision/face-track/tracking.py",
|
|
52
|
+
"nodes/vlm/shot-captions/manifest.yaml",
|
|
53
|
+
"nodes/vlm/shot-captions/node.py",
|
|
54
|
+
"schema/README.md",
|
|
55
|
+
"schema/bundle-0.1.schema.json",
|
|
56
|
+
"sdk/pyproject.toml",
|
|
57
|
+
"sdk/src/perception_node/__init__.py"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# First step on every RunPod pod, before anything is sent (remote_driver.py runs it over ssh):
|
|
3
|
+
# bash -s MIN_DRIVER < cloud/runpod/guard.sh (the oldest NVIDIA driver the nodes' torch runs on, e.g. 580)
|
|
4
|
+
# 1. The pod's removal at its deadline is running: its start command set it (runpodctl, with the pod's own key), the
|
|
5
|
+
# backstop if the laptop never comes back to delete it. That key is scoped to the pod: it removes the pod, and can
|
|
6
|
+
# neither list the account's pods nor remove another (both checked on RunPod, 2026-10-25).
|
|
7
|
+
# 2. No key to the account on the pod: the pod's own key is refused by the API, and there is no RunPod or AWS config.
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
min_driver=$1
|
|
10
|
+
environ=${PERCEPTION_POD_ENVIRON:-/proc/1/environ} # the container's own variables (ssh sessions do not inherit them)
|
|
11
|
+
var() { tr '\0' '\n' < "$environ" | sed -n "s/^$1=//p"; }
|
|
12
|
+
pod=$(var RUNPOD_POD_ID)
|
|
13
|
+
[ -n "$pod" ] || { echo "guard: no RUNPOD_POD_ID in the pod's environment: not a RunPod pod" >&2; exit 1; }
|
|
14
|
+
command -v runpodctl > /dev/null || { echo "guard: runpodctl is missing: the pod could not remove itself at its deadline" >&2; exit 1; }
|
|
15
|
+
for f in ~/.runpod/config.toml ~/.runpod/config.yaml ~/.aws/credentials ~/.aws/config; do
|
|
16
|
+
[ ! -e "$f" ] || { echo "guard: $f exists on the pod: a key to an account must never be there" >&2; exit 1; }
|
|
17
|
+
done
|
|
18
|
+
# its own key: what removes it at the deadline
|
|
19
|
+
[ -n "$(var RUNPOD_API_KEY)" ] || { echo "guard: no RUNPOD_API_KEY in the pod's environment: it could not remove itself at its deadline" >&2; exit 1; }
|
|
20
|
+
# the pod's key must not reach the account: RunPod answers 401 or 403 to it for the account's pods. Exit 75: the check could
|
|
21
|
+
# not be made (no answer, RunPod's own trouble), which says nothing about the key (the driver tries another pod)
|
|
22
|
+
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 30 -A hyperstudy-perception \
|
|
23
|
+
-H "Authorization: Bearer $(var RUNPOD_API_KEY)" "${PERCEPTION_RUNPOD_API:-https://rest.runpod.io/v1}/pods" || true)
|
|
24
|
+
case "$code" in
|
|
25
|
+
401|403) ;;
|
|
26
|
+
2??) echo "guard: the pod's own key got $code listing the account's pods (401 or 403 expected): not used" >&2; exit 1 ;;
|
|
27
|
+
*) echo "guard: could not check the pod's own key (RunPod answered ${code:-nothing}); another pod is tried" >&2; exit 75 ;;
|
|
28
|
+
esac
|
|
29
|
+
# its removal at the deadline, which its start command set before anything else ran (remote_driver.removal_at_boot)
|
|
30
|
+
pgrep -f perception-deadline > /dev/null || { echo "guard: the pod's removal at its deadline is not running" >&2; exit 1; }
|
|
31
|
+
# a driver new enough for the nodes' CUDA, checked here only (RunPod's allowedCudaVersions is not used: see
|
|
32
|
+
# remote_driver.runpod_launch): exit 75, another pod may have one
|
|
33
|
+
driver=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2> /dev/null | head -1 || true)
|
|
34
|
+
[ "${driver%%.*}" -ge "$min_driver" ] 2> /dev/null || {
|
|
35
|
+
echo "guard: the pod's NVIDIA driver is ${driver:-not known}, older than $min_driver (the nodes' CUDA); another pod is tried" >&2; exit 75; }
|
|
36
|
+
gpu=$(nvidia-smi --query-gpu=name --format=csv,noheader 2> /dev/null | head -1 || true)
|
|
37
|
+
echo "guard: ${gpu:-no GPU seen} (driver $driver) in $(var RUNPOD_DC_ID); no key to the account; its removal at its deadline is running"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# The RunPod image for perception's nodes (RunPodBackend.image): software only, no repository code, weights or data
|
|
2
|
+
# (it is public). Built by .github/workflows/runpod-image.yml from the context tag.py stages (these two files).
|
|
3
|
+
#
|
|
4
|
+
# On RunPod's own PyTorch image, which its hosts keep cached: our image's start then pulls only the small layers below
|
|
5
|
+
# (measured: a 6.8 GB image of our own took 145 s to start, the cached one 12-32 s). The nodes' environments are not in
|
|
6
|
+
# it: uv fetches them from PyPI in the datacenter faster than a host pulls them from GHCR. Its Python and torch are
|
|
7
|
+
# not used (uv brings each node's own).
|
|
8
|
+
FROM runpod/pytorch:2.1.0-py3.10-cuda11.8.0-devel-ubuntu22.04@sha256:2594f25beeef751aeebcc207ba4c45a382ba174655f01e15cbbfad0e2bdb5ef8
|
|
9
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
10
|
+
|
|
11
|
+
# what nodes and the driver use (setup then installs nothing); sshd takes keys only. Each pod makes its own host keys
|
|
12
|
+
# at start (start.sh): the ones in the base image are public, and are never used
|
|
13
|
+
RUN apt-get update \
|
|
14
|
+
&& apt-get install -y --no-install-recommends ffmpeg rsync \
|
|
15
|
+
&& rm -rf /var/lib/apt/lists/* /etc/ssh/ssh_host_* \
|
|
16
|
+
&& mkdir -p /run/sshd /etc/ssh/sshd_config.d \
|
|
17
|
+
&& printf 'PermitRootLogin prohibit-password\nPasswordAuthentication no\nKbdInteractiveAuthentication no\n' \
|
|
18
|
+
> /etc/ssh/sshd_config.d/perception.conf \
|
|
19
|
+
&& { grep -q '^Include /etc/ssh/sshd_config.d' /etc/ssh/sshd_config || sed -i '1i Include /etc/ssh/sshd_config.d/*.conf' /etc/ssh/sshd_config; }
|
|
20
|
+
|
|
21
|
+
# runpodctl 3, for the pod's removal at its deadline with its own key (remote_driver.removal_at_boot)
|
|
22
|
+
ARG RUNPODCTL=3.0.1
|
|
23
|
+
ARG RUNPODCTL_SHA256=b66d11eca8cacbf18a2de6943c6e18a79ffbdc8a3c30aa13a49dc6cfc15d4239
|
|
24
|
+
RUN curl -fsSL -o /usr/local/bin/runpodctl https://github.com/runpod/runpodctl/releases/download/v${RUNPODCTL}/runpodctl-linux-amd64 \
|
|
25
|
+
&& echo "${RUNPODCTL_SHA256} /usr/local/bin/runpodctl" | sha256sum -c - \
|
|
26
|
+
&& chmod 755 /usr/local/bin/runpodctl
|
|
27
|
+
|
|
28
|
+
# uv, the version the annotator app bundles, where node-setup.sh puts it
|
|
29
|
+
ARG UV=0.13.5
|
|
30
|
+
ARG UV_SHA256=f68d291c75783122dad58ab77def0e13f3233f358731db5d6d7e8715603b928f
|
|
31
|
+
RUN curl -fsSL -o /tmp/uv.tgz https://github.com/astral-sh/uv/releases/download/${UV}/uv-x86_64-unknown-linux-gnu.tar.gz \
|
|
32
|
+
&& echo "${UV_SHA256} /tmp/uv.tgz" | sha256sum -c - \
|
|
33
|
+
&& mkdir -p /root/.perception-uv \
|
|
34
|
+
&& tar -xzf /tmp/uv.tgz -C /root/.perception-uv --strip-components=1 \
|
|
35
|
+
&& rm /tmp/uv.tgz \
|
|
36
|
+
&& /root/.perception-uv/uv --version
|
|
37
|
+
|
|
38
|
+
COPY start.sh /perception-start.sh
|
|
39
|
+
CMD ["/perception-start.sh"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# A pod's start (the image's CMD; remote_driver.removal_at_boot runs it after setting the pod's removal): ssh host keys
|
|
3
|
+
# of its own, the key the driver made for it (PUBLIC_KEY, from RunPod), and sshd. Nothing else runs until the driver
|
|
4
|
+
# connects.
|
|
5
|
+
set -eu
|
|
6
|
+
ssh-keygen -A
|
|
7
|
+
mkdir -p /root/.ssh
|
|
8
|
+
chmod 700 /root/.ssh
|
|
9
|
+
printf '%s\n' "${PUBLIC_KEY:?the pod was given no PUBLIC_KEY}" > /root/.ssh/authorized_keys
|
|
10
|
+
chmod 600 /root/.ssh/authorized_keys
|
|
11
|
+
exec /usr/sbin/sshd -D -e
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""The RunPod image's tag, from everything that goes into it (its Dockerfile and start.sh), and its build context:
|
|
2
|
+
|
|
3
|
+
python3 cloud/runpod/image/tag.py prints the tag (12 hex digits)
|
|
4
|
+
python3 cloud/runpod/image/tag.py --context DIR also copies exactly those files into DIR (what is built)
|
|
5
|
+
|
|
6
|
+
The same files give the same tag: RunPodBackend.image_tag must equal it (a test checks), so a change to the image needs
|
|
7
|
+
a new build (.github/workflows/runpod-image.yml) and that tag. Standard library only."""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import shutil
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
HERE = Path(__file__).resolve().parent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def files() -> dict[str, Path]:
|
|
18
|
+
"""Each file in the context, by its path there."""
|
|
19
|
+
return {name: HERE / name for name in ("Dockerfile", "start.sh")}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def tag() -> str:
|
|
23
|
+
h = hashlib.sha256()
|
|
24
|
+
for name, path in sorted(files().items()):
|
|
25
|
+
h.update(name.encode() + b"\0" + path.read_bytes() + b"\0")
|
|
26
|
+
return h.hexdigest()[:12]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def stage(dest: Path):
|
|
30
|
+
for name, path in files().items():
|
|
31
|
+
(dest / name).parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
shutil.copy2(path, dest / name)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
if len(sys.argv) == 3 and sys.argv[1] == "--context":
|
|
37
|
+
stage(Path(sys.argv[2]))
|
|
38
|
+
elif len(sys.argv) != 1:
|
|
39
|
+
sys.exit(__doc__)
|
|
40
|
+
print(tag())
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# First NVIDIA run: decode + face tracks on one clip, on an L4 or A10G machine. The clip is copied to the machine only
|
|
2
|
+
# (file mount from the laptop) and deleted with it. Results are exported to ~/out for the laptop to fetch before the
|
|
3
|
+
# cluster is deleted. Sending a study's stimulus to a cloud machine needs that study's approval: a whole graph run
|
|
4
|
+
# off the laptop is not checked by the graph's privacy policy.
|
|
5
|
+
# cloud/skypilot/launch.sh cloud/skypilot/gpu-faces.yaml perception-gpu --env CLIP_SRC=/path/to/clip.mp4
|
|
6
|
+
resources:
|
|
7
|
+
cloud: aws
|
|
8
|
+
region: us-east-1
|
|
9
|
+
# L4 (g6) or A10G (g5): both 24 GB. On 2026-10-23 us-east-1 had no g6.xlarge spot capacity in any zone, and the
|
|
10
|
+
# account's G spot quota is 0 outside us-east-1.
|
|
11
|
+
accelerators: {L4: 1, A10G: 1}
|
|
12
|
+
use_spot: true
|
|
13
|
+
disk_size: 60
|
|
14
|
+
|
|
15
|
+
envs:
|
|
16
|
+
CLIP_SRC: null # required: the clip on the laptop
|
|
17
|
+
|
|
18
|
+
file_mounts:
|
|
19
|
+
/data/clip.mp4: ${CLIP_SRC}
|
|
20
|
+
|
|
21
|
+
setup: |
|
|
22
|
+
set -e
|
|
23
|
+
bash cloud/skypilot/guard.sh 90
|
|
24
|
+
sudo apt-get update -qq && sudo apt-get install -y -qq ffmpeg > /dev/null
|
|
25
|
+
command -v uv || curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
26
|
+
export PATH=$HOME/.local/bin:$PATH
|
|
27
|
+
(cd orchestrator && uv sync -q) && (cd envs/core && uv sync -q) && (cd envs/faces && uv sync -q)
|
|
28
|
+
|
|
29
|
+
run: |
|
|
30
|
+
set -e
|
|
31
|
+
export PATH=$HOME/.local/bin:$PATH
|
|
32
|
+
nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv
|
|
33
|
+
(cd envs/faces && uv run python -c "import torch; print('torch', torch.__version__, 'cuda', torch.cuda.is_available(), torch.version.cuda)")
|
|
34
|
+
cd orchestrator
|
|
35
|
+
uv run perception doctor
|
|
36
|
+
time uv run perception run ../graphs/faces.yaml --input video=/data/clip.mp4 --allow-downloads
|
|
37
|
+
run=$(uv run perception status | awk 'NR==1{print $1}')
|
|
38
|
+
uv run perception export "$run" --output ~/out/bundle
|
|
39
|
+
cp -r ~/.hyperstudy-perception/work/"$run" ~/out/work
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# What limits the face node on an L4: one machine, one decode, then the face node at several batch sizes and numbers
|
|
2
|
+
# of frame-decoding workers, with GPU utilisation logged every second. g6.2xlarge (8 vCPUs), so batch 32 with no
|
|
3
|
+
# workers compares directly with the first run on a g6.xlarge (4 vCPUs). Results go to ~/out for the laptop to fetch.
|
|
4
|
+
# Copies the clip to the machine, like gpu-faces.yaml (see there on approval).
|
|
5
|
+
# cloud/skypilot/launch.sh cloud/skypilot/gpu-tune.yaml perception-tune --env CLIP_SRC=/path/to/clip.mp4
|
|
6
|
+
resources:
|
|
7
|
+
cloud: aws
|
|
8
|
+
region: us-east-1
|
|
9
|
+
accelerators: L4:1
|
|
10
|
+
cpus: 8
|
|
11
|
+
use_spot: false # no L4 spot capacity in us-east-1 on 2026-10-23
|
|
12
|
+
disk_size: 60
|
|
13
|
+
|
|
14
|
+
envs:
|
|
15
|
+
CLIP_SRC: null # required: the clip on the laptop
|
|
16
|
+
# batch_size:num_workers. Batch 64 ran out of GPU memory on 2026-10-24 (py-feat feeds whole 1080p frames).
|
|
17
|
+
SWEEP: "8:0 16:0 32:0 16:4 32:4"
|
|
18
|
+
|
|
19
|
+
file_mounts:
|
|
20
|
+
/data/clip.mp4: ${CLIP_SRC}
|
|
21
|
+
|
|
22
|
+
setup: |
|
|
23
|
+
set -e
|
|
24
|
+
bash cloud/skypilot/guard.sh 90
|
|
25
|
+
sudo apt-get update -qq && sudo apt-get install -y -qq ffmpeg > /dev/null
|
|
26
|
+
command -v uv || curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
27
|
+
export PATH=$HOME/.local/bin:$PATH
|
|
28
|
+
(cd orchestrator && uv sync -q) && (cd envs/core && uv sync -q) && (cd envs/faces && uv sync -q)
|
|
29
|
+
|
|
30
|
+
run: |
|
|
31
|
+
set -e
|
|
32
|
+
export PATH=$HOME/.local/bin:$PATH
|
|
33
|
+
mkdir -p ~/out
|
|
34
|
+
# Background processes end with the job however it ends: a GPU logger left running kept a failed job open on
|
|
35
|
+
# 2026-10-24 until it was cancelled by hand.
|
|
36
|
+
trap 'kill $(jobs -p) 2>/dev/null || :' EXIT
|
|
37
|
+
nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv | tee ~/out/gpu.txt
|
|
38
|
+
# Ray, which runs SkyPilot jobs, sets OMP_NUM_THREADS=1, and PyTorch takes its CPU thread count from it.
|
|
39
|
+
echo "vCPUs $(nproc --all); OMP_NUM_THREADS from the job: ${OMP_NUM_THREADS:-unset}" | tee -a ~/out/gpu.txt
|
|
40
|
+
export OMP_NUM_THREADS=$(nproc --all)
|
|
41
|
+
nvidia-smi dmon -s um -o T > ~/out/dmon.txt 2>&1 &
|
|
42
|
+
cd orchestrator
|
|
43
|
+
uv run perception run ../graphs/decode.yaml --input video=/data/clip.mp4
|
|
44
|
+
for point in $SWEEP; do
|
|
45
|
+
b=${point%:*}; w=${point#*:}
|
|
46
|
+
sed "s/uses: vision.face-track@1.0.0/&\n config: {batch_size: $b, num_workers: $w}/" ../graphs/faces.yaml > ../graphs/tune-$b-$w.yaml
|
|
47
|
+
echo "=== batch $b workers $w $(date +%T)" | tee -a ~/out/sweep.txt
|
|
48
|
+
SECONDS=0
|
|
49
|
+
# A failed point is recorded (with its error code, from status) and the sweep goes on.
|
|
50
|
+
if uv run perception run ../graphs/tune-$b-$w.yaml --input video=/data/clip.mp4 --allow-downloads; then result=ok; else result=failed; fi
|
|
51
|
+
echo "wall ${SECONDS}s $result end $(date +%T)" | tee -a ~/out/sweep.txt
|
|
52
|
+
run=$(uv run perception status | awk 'NR==1{print $1}')
|
|
53
|
+
uv run perception status "$run" | tee -a ~/out/sweep.txt
|
|
54
|
+
cp -r ~/.hyperstudy-perception/work/"$run" ~/out/work-$b-$w
|
|
55
|
+
done
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# First step of every task's setup: a shutdown deadline, and a check that the machine holds no AWS credentials
|
|
3
|
+
# and no role beyond perception-node.
|
|
4
|
+
# bash cloud/skypilot/guard.sh MINUTES
|
|
5
|
+
# The deadline is the backstop if the laptop never comes back to delete the machine. launch.sh makes an OS shutdown
|
|
6
|
+
# terminate the instance (EC2's default for on-demand machines is to stop it, which keeps the disk billing).
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
sudo shutdown -h "+$1"
|
|
9
|
+
if [ -e ~/.aws/credentials ] || [ -e ~/.aws/config ]; then
|
|
10
|
+
echo "guard: ~/.aws exists on the machine; sky.yaml was not applied" >&2; exit 1
|
|
11
|
+
fi
|
|
12
|
+
token=$(curl -sf -X PUT http://169.254.169.254/latest/api/token -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
|
|
13
|
+
# The only role allowed is perception-node, which can end perception-* machines and nothing else (sky.yaml).
|
|
14
|
+
profile=$(curl -sf -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/iam/info \
|
|
15
|
+
| python3 -c 'import json,sys; print(json.load(sys.stdin)["InstanceProfileArn"])' || echo none)
|
|
16
|
+
case "$profile" in
|
|
17
|
+
*/perception-node) ;;
|
|
18
|
+
*) echo "guard: the instance profile is $profile, not perception-node; sky.yaml was not applied" >&2; exit 1 ;;
|
|
19
|
+
esac
|
|
20
|
+
echo "guard: no AWS credentials; role perception-node; shutdown in $1 min"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Version": "2012-10-17",
|
|
3
|
+
"Statement": [
|
|
4
|
+
{
|
|
5
|
+
"Sid": "FindTheCluster",
|
|
6
|
+
"Effect": "Allow",
|
|
7
|
+
"Action": ["ec2:DescribeInstances", "ec2:DescribeSecurityGroups", "ec2:DescribeVpcs"],
|
|
8
|
+
"Resource": "*"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"Sid": "EndPerceptionMachinesOnly",
|
|
12
|
+
"Effect": "Allow",
|
|
13
|
+
"Action": "ec2:TerminateInstances",
|
|
14
|
+
"Resource": "arn:aws:ec2:*:*:instance/*",
|
|
15
|
+
"Condition": {"StringLike": {"aws:ResourceTag/skypilot-cluster-name": "perception-*"}}
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run one task on AWS and delete the machine afterwards.
|
|
3
|
+
# [OUT=DIR] cloud/skypilot/launch.sh TASK.yaml CLUSTER [sky launch options, e.g. --env CLIP_SRC=/path/to/clip.mp4]
|
|
4
|
+
# With OUT set, the machine's ~/out is copied into DIR before the machine is deleted (also when the job failed).
|
|
5
|
+
# - SkyPilot reads sky.yaml as the project config: no credentials copied, and only the perception-node role.
|
|
6
|
+
# - The workdir is a staged copy of the files git tracks, nothing else: commit a new file before a launch that needs
|
|
7
|
+
# it. (Untracked files were staged too until 2026-10-24; .gitignore does not cover every image, crop or bundle
|
|
8
|
+
# folder that can sit in a working copy.)
|
|
9
|
+
# - Once the instance exists, its OS shutdown is set to terminate it, so guard.sh's deadline deletes on-demand
|
|
10
|
+
# machines as well as spot ones.
|
|
11
|
+
# - sky down runs on exit, whether the job succeeded, failed or was interrupted.
|
|
12
|
+
# - A wall-clock deadline (MAX_MINUTES, default 60) ends the launch, and so deletes the machine, whatever the job is
|
|
13
|
+
# doing: a hang of any kind (a stray process holding the job open, a stuck download) costs at most that long.
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
task=$(cd "$(dirname "$1")" && pwd)/$(basename "$1"); cluster=$2; shift 2
|
|
16
|
+
case "$cluster" in
|
|
17
|
+
perception-*) ;;
|
|
18
|
+
*) echo "launch.sh: cluster names start with perception- (the machine's role can only end those machines)" >&2; exit 2 ;;
|
|
19
|
+
esac
|
|
20
|
+
repo=$(git -C "$(dirname "$0")" rev-parse --show-toplevel)
|
|
21
|
+
region=us-east-1
|
|
22
|
+
export SKYPILOT_PROJECT_CONFIG=$repo/cloud/skypilot/sky.yaml
|
|
23
|
+
|
|
24
|
+
stage=$(mktemp -d)
|
|
25
|
+
git -C "$repo" ls-files -z | rsync -a --from0 --files-from=- "$repo/" "$stage/"
|
|
26
|
+
finish() {
|
|
27
|
+
kill "${watcher:-}" "${deadline:-}" 2>/dev/null || :
|
|
28
|
+
if [ -n "${OUT:-}" ]; then mkdir -p "$OUT"; rsync -a "$cluster:out/" "$OUT/" || echo "launch.sh: could not fetch ~/out" >&2; fi
|
|
29
|
+
sky down -y "$cluster"
|
|
30
|
+
rm -rf "$stage"
|
|
31
|
+
}
|
|
32
|
+
trap finish EXIT
|
|
33
|
+
|
|
34
|
+
# SkyPilot provisions the instance a few seconds to minutes into `sky launch`; set its shutdown behaviour as soon as it
|
|
35
|
+
# exists. Gives up loudly after 15 minutes (then only `sky down`, autodown or MAX_MINUTES deletes it). Not under
|
|
36
|
+
# set -e: one failed AWS call must not end the watcher silently.
|
|
37
|
+
(
|
|
38
|
+
set +e
|
|
39
|
+
for _ in $(seq 90); do
|
|
40
|
+
ids=$(aws ec2 describe-instances --region "$region" --output text --query 'Reservations[].Instances[].InstanceId' \
|
|
41
|
+
--filters "Name=tag:skypilot-cluster-name,Values=$cluster*" Name=instance-state-name,Values=pending,running)
|
|
42
|
+
if [ -n "$ids" ]; then
|
|
43
|
+
for id in $ids; do
|
|
44
|
+
if aws ec2 modify-instance-attribute --region "$region" --instance-id "$id" --instance-initiated-shutdown-behavior terminate; then
|
|
45
|
+
echo "launch.sh: $id terminates on OS shutdown"
|
|
46
|
+
else
|
|
47
|
+
echo "launch.sh: WARNING could not make $id terminate on shutdown; its deadline will only stop it" >&2
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
sleep 10
|
|
53
|
+
done
|
|
54
|
+
echo "launch.sh: WARNING no instance for $cluster after 15 min; its shutdown deadline will only stop it" >&2
|
|
55
|
+
) &
|
|
56
|
+
watcher=$!
|
|
57
|
+
|
|
58
|
+
sky launch -c "$cluster" -i 15 --down -y "$task" --workdir "$stage" "$@" &
|
|
59
|
+
launch=$!
|
|
60
|
+
minutes=${MAX_MINUTES:-60}
|
|
61
|
+
( sleep $((minutes * 60)); echo "launch.sh: past MAX_MINUTES=$minutes; deleting the machine" >&2; kill "$launch" ) &
|
|
62
|
+
deadline=$!
|
|
63
|
+
wait "$launch"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# The machine the cloud backend (orchestrator/src/perception/backends/cloud.py) starts for one node, and deletes after it:
|
|
2
|
+
# an L4 with 8 vCPUs, the face node's fastest measured setup (perception #4; frame decoding needs the cores). Only
|
|
3
|
+
# set-up here: the driver checks the machine with guard.sh before sending anything, then runs the node over SSH.
|
|
4
|
+
# Started with SKYPILOT_PROJECT_CONFIG=sky.yaml (no laptop credentials on the machine; only the perception-node role).
|
|
5
|
+
resources:
|
|
6
|
+
cloud: aws
|
|
7
|
+
region: us-east-1
|
|
8
|
+
instance_type: g6.2xlarge
|
|
9
|
+
# on-demand: on 2026-10-23 us-east-1 had no L4 or A10G spot capacity, and the account's G spot quota is 0 elsewhere
|
|
10
|
+
use_spot: false
|
|
11
|
+
disk_size: 60
|
|
12
|
+
|
|
13
|
+
envs:
|
|
14
|
+
DEADLINE_EPOCH: null # required: the driver passes the node's deadline, as a moment (Unix seconds)
|
|
15
|
+
|
|
16
|
+
setup: |
|
|
17
|
+
# SkyPilot runs setup detached (as a job: `sky launch` returns before it ends), so only what must come first is here:
|
|
18
|
+
# the machine shuts itself down (terminating: the driver sets that while it launches) at the deadline, even if the
|
|
19
|
+
# laptop that started it never comes back. guard.sh later sets it again, to what is left. What the node needs (ffmpeg,
|
|
20
|
+
# uv) the driver installs itself and waits for: node-setup.sh.
|
|
21
|
+
# Up to the moment, not a fixed delay: this runs detached, perhaps after guard.sh, and must not move the deadline later.
|
|
22
|
+
# Whole minutes left, rounded down; none (shut down now) once it has passed. (`shutdown HH:MM` would take a time
|
|
23
|
+
# already gone as tomorrow's.)
|
|
24
|
+
left=$(( (DEADLINE_EPOCH - $(date +%s)) / 60 )); [ "$left" -gt 0 ] || left=0
|
|
25
|
+
sudo shutdown -h "+$left"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# What a cloud node needs on its machine (AWS or RunPod), installed by the driver (remote_driver.py) over ssh after guard.sh
|
|
3
|
+
# and before anything is sent, and waited for: SkyPilot runs a task's setup detached (as a job, `sky launch` returns
|
|
4
|
+
# before it ends), so a node started after the launch could find ffmpeg half installed.
|
|
5
|
+
# bash -s < node-setup.sh prints the uv it installed on its last line
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
8
|
+
sudo=sudo; [ "$(id -u)" = 0 ] && sudo= # a RunPod pod is root, with no sudo
|
|
9
|
+
# a fresh Ubuntu machine runs its own updates at boot: wait for apt's lock rather than fail on it. rsync: the driver sends
|
|
10
|
+
# with it (RunPod's image has none)
|
|
11
|
+
if ! command -v ffmpeg > /dev/null || ! command -v ffprobe > /dev/null || ! command -v rsync > /dev/null; then # an image may have them
|
|
12
|
+
$sudo apt-get -o DPkg::Lock::Timeout=600 update -qq
|
|
13
|
+
$sudo apt-get -o DPkg::Lock::Timeout=600 install -y -qq ffmpeg rsync > /dev/null
|
|
14
|
+
fi
|
|
15
|
+
# uv, the version the annotator app bundles, in a folder of its own (SkyPilot installs a uv of its own on the machine)
|
|
16
|
+
dir="$HOME/.perception-uv"
|
|
17
|
+
if [ ! -x "$dir/uv" ]; then
|
|
18
|
+
curl -LsSf https://astral.sh/uv/0.13.5/install.sh | env UV_INSTALL_DIR="$dir" UV_NO_MODIFY_PATH=1 sh > /dev/null
|
|
19
|
+
fi
|
|
20
|
+
"$dir/uv" --version | grep -q '^uv 0\.13\.5' || { echo "node-setup: $dir/uv is not uv 0.13.5" >&2; exit 1; }
|
|
21
|
+
command -v ffmpeg > /dev/null && command -v ffprobe > /dev/null && command -v rsync > /dev/null
|
|
22
|
+
echo "$dir/uv"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# SkyPilot project config for this repo's tasks; launch.sh points SKYPILOT_PROJECT_CONFIG here. A task YAML cannot set
|
|
2
|
+
# this for AWS (SkyPilot only lets tasks override remote_identity on Kubernetes and Azure).
|
|
3
|
+
aws:
|
|
4
|
+
# An instance profile that can only end perception-* machines (iam-perception-node.json). Naming one does two
|
|
5
|
+
# things: no laptop credentials are copied to the machine, and SkyPilot does not attach its default skypilot-v1
|
|
6
|
+
# role (EC2 and S3 full access), which it does even with NO_UPLOAD. guard.sh checks both on the machine.
|
|
7
|
+
remote_identity: perception-node
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Smoke test of a cloud machine: install the orchestrator, run the decode graph on a synthetic video (ffmpeg test
|
|
2
|
+
# pattern and tone, so no stimulus leaves the laptop), print timings, then the cluster is deleted.
|
|
3
|
+
# cloud/skypilot/launch.sh cloud/skypilot/smoke.yaml perception-smoke
|
|
4
|
+
# --cpus/--gpus after the cluster name pick the machine; the defaults are the cheapest spot CPU machine.
|
|
5
|
+
resources:
|
|
6
|
+
cloud: aws
|
|
7
|
+
region: us-east-1
|
|
8
|
+
cpus: 2+
|
|
9
|
+
use_spot: true
|
|
10
|
+
|
|
11
|
+
setup: |
|
|
12
|
+
set -e
|
|
13
|
+
bash cloud/skypilot/guard.sh 45
|
|
14
|
+
sudo apt-get update -qq && sudo apt-get install -y -qq ffmpeg > /dev/null
|
|
15
|
+
command -v uv || curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
16
|
+
export PATH=$HOME/.local/bin:$PATH
|
|
17
|
+
(cd orchestrator && uv sync -q) && (cd envs/core && uv sync -q)
|
|
18
|
+
|
|
19
|
+
run: |
|
|
20
|
+
set -e
|
|
21
|
+
export PATH=$HOME/.local/bin:$PATH
|
|
22
|
+
nproc; free -g | head -2; nvidia-smi --query-gpu=name,memory.total --format=csv 2>/dev/null || echo "no GPU"
|
|
23
|
+
ffmpeg -v error -y -f lavfi -i testsrc2=size=1280x720:rate=30:duration=120 -f lavfi -i sine=frequency=440:duration=120 \
|
|
24
|
+
-c:v libx264 -pix_fmt yuv420p -c:a aac -shortest /tmp/synthetic.mp4
|
|
25
|
+
cd orchestrator
|
|
26
|
+
uv run perception doctor
|
|
27
|
+
time uv run perception run ../graphs/decode.yaml --input video=/tmp/synthetic.mp4
|
|
28
|
+
uv run perception status
|