simulo 0.25.0__tar.gz → 0.27.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.
- {simulo-0.25.0/src/simulo.egg-info → simulo-0.27.0}/PKG-INFO +15 -2
- {simulo-0.25.0 → simulo-0.27.0}/PYPI.md +13 -0
- {simulo-0.25.0 → simulo-0.27.0}/pyproject.toml +2 -2
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/__init__.py +6 -5
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/app.py +81 -12
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/cli.py +137 -9
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/http.py +3 -1
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/install_samples.py +138 -4
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/preflight_api.py +4 -12
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/submit_api.py +11 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/templates/inference/app.py.tmpl +4 -3
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/templates/scenario/app.py.tmpl +2 -1
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/templates/training/app.py.tmpl +2 -1
- {simulo-0.25.0 → simulo-0.27.0/src/simulo.egg-info}/PKG-INFO +15 -2
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo.egg-info/requires.txt +1 -1
- {simulo-0.25.0 → simulo-0.27.0}/MANIFEST.in +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/setup.cfg +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/__init__.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/_entrypoint.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/_mounts.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/_runner.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/_secure_downloads.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/asset.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/asset_api.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/asset_package.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/asset_pins.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/builtin_aliases.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/bundle.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/cancel_api.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/config.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/credentials.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/discovery.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/export_api.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/export_bundle.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/facades.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/identity_api.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/jobs_api.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/learning.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/login.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/mode.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/outputs.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/packaging.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/preflight_render.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/registry.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/runtime.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/runtime_display.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/seed_ref.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/stub.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/templates/__init__.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/templates/inference/simuloignore.tmpl +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/templates/scenario/simuloignore.tmpl +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/templates/training/simuloignore.tmpl +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/view_fragment.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/view_session_api.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/_client/volume.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/callbacks.py +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo/py.typed +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo.egg-info/SOURCES.txt +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo.egg-info/dependency_links.txt +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo.egg-info/entry_points.txt +0 -0
- {simulo-0.25.0 → simulo-0.27.0}/src/simulo.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simulo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.27.0
|
|
4
4
|
Summary: Simulo SDK and CLI — define robotics simulation and training apps in Python and run them on the Simulo cloud.
|
|
5
5
|
Author-email: Simulo Team <team@simulo.ai>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Typing :: Typed
|
|
16
16
|
Requires-Python: >=3.11
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
-
Requires-Dist: simulo-interfaces<0.
|
|
18
|
+
Requires-Dist: simulo-interfaces<0.20,>=0.19.0
|
|
19
19
|
Requires-Dist: mcap<2,>=1.3
|
|
20
20
|
Requires-Dist: defusedxml>=0.7.1
|
|
21
21
|
Requires-Dist: usd-core<27,>=26.5
|
|
@@ -75,6 +75,19 @@ To download a curated collection of runnable projects, use `simulo install sampl
|
|
|
75
75
|
It creates `./simulo-samples` as a normal Git clone, so `cd simulo-samples` and
|
|
76
76
|
`cat README.md` open the sample index.
|
|
77
77
|
|
|
78
|
+
Choose a destination with `--directory` and a Git branch, tag, or commit with
|
|
79
|
+
`--ref`. You can use either option on its own or combine them:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
simulo install samples --directory robotics-examples --ref main
|
|
83
|
+
cd robotics-examples
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The destination must not exist, and its parent directory must already exist.
|
|
87
|
+
The directory appears only after the clone and requested checkout succeed.
|
|
88
|
+
Use a tag or full commit ID instead of `main` to select a fixed revision.
|
|
89
|
+
With `--ref`, the checkout is detached at the selected commit.
|
|
90
|
+
|
|
78
91
|
### Or write an app by hand
|
|
79
92
|
|
|
80
93
|
```python
|
|
@@ -35,6 +35,19 @@ To download a curated collection of runnable projects, use `simulo install sampl
|
|
|
35
35
|
It creates `./simulo-samples` as a normal Git clone, so `cd simulo-samples` and
|
|
36
36
|
`cat README.md` open the sample index.
|
|
37
37
|
|
|
38
|
+
Choose a destination with `--directory` and a Git branch, tag, or commit with
|
|
39
|
+
`--ref`. You can use either option on its own or combine them:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
simulo install samples --directory robotics-examples --ref main
|
|
43
|
+
cd robotics-examples
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The destination must not exist, and its parent directory must already exist.
|
|
47
|
+
The directory appears only after the clone and requested checkout succeed.
|
|
48
|
+
Use a tag or full commit ID instead of `main` to select a fixed revision.
|
|
49
|
+
With `--ref`, the checkout is detached at the selected commit.
|
|
50
|
+
|
|
38
51
|
### Or write an app by hand
|
|
39
52
|
|
|
40
53
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "simulo"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.27.0"
|
|
8
8
|
description = "Simulo SDK and CLI — define robotics simulation and training apps in Python and run them on the Simulo cloud."
|
|
9
9
|
readme = "PYPI.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
# mcap: used by `simulo recordings` to read back a downloaded recording and
|
|
27
27
|
# verify it (message count) — MIT-licensed, deps only lz4+zstandard.
|
|
28
28
|
dependencies = [
|
|
29
|
-
"simulo-interfaces>=0.
|
|
29
|
+
"simulo-interfaces>=0.19.0,<0.20",
|
|
30
30
|
"mcap>=1.3,<2",
|
|
31
31
|
# Hardened XML parsing for URDF ingestion (USD Asset Catalogs, PR-12 fix
|
|
32
32
|
# loop 2, security NIT): stdlib xml.etree.ElementTree relies on
|
|
@@ -29,10 +29,10 @@ missed that way before the rule replaced it.
|
|
|
29
29
|
Three kinds of name live on ``simulo``:
|
|
30
30
|
|
|
31
31
|
* **Platform** names — ``App / Runtime / Volume / Asset / Output /
|
|
32
|
-
OutputKind / callbacks`` — are **real, eager** attributes bound
|
|
33
|
-
They are always torch-free (``Output`` is the ``simulo-interfaces``
|
|
34
|
-
contract dataclass, ``OutputKind`` its wire-stable
|
|
35
|
-
re-exported).
|
|
32
|
+
OutputKind / SystemType / callbacks`` — are **real, eager** attributes bound
|
|
33
|
+
below. They are always torch-free (``Output`` is the ``simulo-interfaces``
|
|
34
|
+
contract dataclass, ``OutputKind`` and ``SystemType`` its wire-stable
|
|
35
|
+
``StrEnum``\\ s, all three re-exported).
|
|
36
36
|
* **Contract vocabulary** — the shared error hierarchy (``SimuloError`` and its
|
|
37
37
|
subclasses), the ``NewType`` identifier aliases (``JobId``, ``ProjectId``, …),
|
|
38
38
|
and the authoring value types that are not already carried by the lazy
|
|
@@ -152,7 +152,7 @@ from simulo.interfaces.authoring import (
|
|
|
152
152
|
USDWorld,
|
|
153
153
|
VisualMarkerBase,
|
|
154
154
|
)
|
|
155
|
-
from simulo.interfaces.platform import Output, OutputKind
|
|
155
|
+
from simulo.interfaces.platform import Output, OutputKind, SystemType
|
|
156
156
|
|
|
157
157
|
# Public surface = eager platform exports (real attributes; ``__getattr__`` never
|
|
158
158
|
# fires for these) + the lazily-resolved learning names. Kept a static literal so
|
|
@@ -162,6 +162,7 @@ __all__ = [
|
|
|
162
162
|
"App",
|
|
163
163
|
"Output",
|
|
164
164
|
"OutputKind",
|
|
165
|
+
"SystemType",
|
|
165
166
|
"Runtime",
|
|
166
167
|
"Volume",
|
|
167
168
|
"Asset",
|
|
@@ -41,7 +41,8 @@ from simulo._client.runtime import DEFAULT_RUNTIME
|
|
|
41
41
|
from simulo.interfaces.ids import JobId, parse_job_public_id
|
|
42
42
|
from simulo.interfaces.platform import JobCallbackProtocol
|
|
43
43
|
from simulo.interfaces.platform.app import Mount
|
|
44
|
-
from simulo.interfaces.platform.enums import ResumePolicy
|
|
44
|
+
from simulo.interfaces.platform.enums import ResumePolicy, SystemType
|
|
45
|
+
from simulo.interfaces.platform.systems import SYSTEM_SPECS
|
|
45
46
|
|
|
46
47
|
if TYPE_CHECKING: # typing-only; avoids a module-scope dependency on the HTTP client
|
|
47
48
|
from simulo._client.preflight_api import PreflightOutcome
|
|
@@ -427,6 +428,14 @@ def _spawn_cloud(
|
|
|
427
428
|
content-addressed identity must never change based on whether this
|
|
428
429
|
particular execution asked for a live stream.
|
|
429
430
|
|
|
431
|
+
The requested GPU system tier (system-tier-catalog-1362, M50-persistence)
|
|
432
|
+
is NOT a parameter here — it is read from the already-parsed manifest's
|
|
433
|
+
``jobs[job_name]["resources"]["system"]`` (the wire value
|
|
434
|
+
``@app.job(system=...)`` already validated and recorded at decoration
|
|
435
|
+
time) and forwarded to ``create_job`` below as an optional top-level
|
|
436
|
+
sibling of ``args``, for the identical package-hash-invariance reason as
|
|
437
|
+
``viewstream``.
|
|
438
|
+
|
|
430
439
|
``seed_from`` (``simulo run --from``, explicit-run-intent plan) follows
|
|
431
440
|
the identical rule: the seed reference is a SUBMIT-time job property,
|
|
432
441
|
never a package property — it rides the job-create call only, verbatim
|
|
@@ -496,6 +505,15 @@ def _spawn_cloud(
|
|
|
496
505
|
package_id = str(package_id)
|
|
497
506
|
source_digest = str(manifest.get("source_digest", ""))
|
|
498
507
|
args = dict(manifest.get("args") or {})
|
|
508
|
+
# The requested GPU system tier (system-tier-catalog-1362, M50-persistence)
|
|
509
|
+
# — already validated at `@app.job(system=...)` decoration time and
|
|
510
|
+
# recorded on THIS job's own manifest entry as `resources["system"]`
|
|
511
|
+
# (the tier's wire value, e.g. "tier1"; see `decorate()`'s
|
|
512
|
+
# `resources["system"] = system.value`). Read from the already-parsed
|
|
513
|
+
# manifest rather than threaded in as a new parameter, since the value
|
|
514
|
+
# already lives there, keyed by this same `job_name`.
|
|
515
|
+
job_entry = manifest.get("jobs", {}).get(job_name) or {}
|
|
516
|
+
system = (job_entry.get("resources") or {}).get("system")
|
|
499
517
|
|
|
500
518
|
tar_path = ensure_tar(package_path)
|
|
501
519
|
archive_bytes = tar_path.read_bytes()
|
|
@@ -549,6 +567,7 @@ def _spawn_cloud(
|
|
|
549
567
|
seed_from_job_id=seed_from,
|
|
550
568
|
assets=asset_pins,
|
|
551
569
|
app_name=app_name,
|
|
570
|
+
system=system,
|
|
552
571
|
)
|
|
553
572
|
|
|
554
573
|
raw_public_id = record.get("public_id")
|
|
@@ -970,7 +989,7 @@ class App:
|
|
|
970
989
|
|
|
971
990
|
app = simulo.App("cartpole")
|
|
972
991
|
|
|
973
|
-
@app.job(
|
|
992
|
+
@app.job(system=simulo.SystemType.TIER_1)
|
|
974
993
|
def train(iterations: int = 1000):
|
|
975
994
|
...
|
|
976
995
|
|
|
@@ -1044,7 +1063,7 @@ class App:
|
|
|
1044
1063
|
*,
|
|
1045
1064
|
callbacks: Sequence[JobCallbackProtocol] = ...,
|
|
1046
1065
|
resume: ResumePolicy = ...,
|
|
1047
|
-
|
|
1066
|
+
system: Optional[SystemType] = ...,
|
|
1048
1067
|
timeout: Optional[int] = ...,
|
|
1049
1068
|
retries: int = ...,
|
|
1050
1069
|
**extra: Any,
|
|
@@ -1056,14 +1075,15 @@ class App:
|
|
|
1056
1075
|
*,
|
|
1057
1076
|
callbacks: Sequence[JobCallbackProtocol] = (),
|
|
1058
1077
|
resume: ResumePolicy = ResumePolicy.AUTO,
|
|
1059
|
-
|
|
1078
|
+
system: Optional[SystemType] = None,
|
|
1060
1079
|
timeout: Optional[int] = None,
|
|
1061
1080
|
retries: int = 0,
|
|
1062
1081
|
**extra: Any,
|
|
1063
1082
|
) -> Union[JobFunction, Callable[[F], JobFunction]]:
|
|
1064
1083
|
"""Register a function as a job of this app. Records metadata; never runs the body.
|
|
1065
1084
|
|
|
1066
|
-
Usable bare (``@app.job``) or with options
|
|
1085
|
+
Usable bare (``@app.job``) or with options
|
|
1086
|
+
(``@app.job(system=simulo.SystemType.TIER_1)``).
|
|
1067
1087
|
Returns a :class:`JobFunction` wrapping the original function — calling
|
|
1068
1088
|
it still runs the body in-process; ``.spawn()`` / ``.submit()`` submit it
|
|
1069
1089
|
to the platform instead.
|
|
@@ -1077,8 +1097,17 @@ class App:
|
|
|
1077
1097
|
resume: How the job resumes from a discovered checkpoint
|
|
1078
1098
|
(:class:`~simulo.interfaces.platform.enums.ResumePolicy`,
|
|
1079
1099
|
default ``AUTO``).
|
|
1080
|
-
|
|
1081
|
-
|
|
1100
|
+
system: The GPU system tier to request, as a
|
|
1101
|
+
:class:`simulo.SystemType` member (e.g.
|
|
1102
|
+
``simulo.SystemType.TIER_1``), recorded in the job's resource
|
|
1103
|
+
spec as ``resources["system"]`` (the tier's wire value, e.g.
|
|
1104
|
+
``"tier1"``). Validated HERE, at decoration time, against the
|
|
1105
|
+
published catalog: a value that is not a ``SystemType`` member
|
|
1106
|
+
(a raw string such as ``"tier1"`` included) raises
|
|
1107
|
+
``TypeError``, and a tier the platform cannot provision yet
|
|
1108
|
+
raises ``ValueError``. ``simulo systems`` lists every tier and
|
|
1109
|
+
which are available today. The selection is recorded on the
|
|
1110
|
+
job; it does not yet choose hardware.
|
|
1082
1111
|
timeout: Wall-clock limit for one managed execution, in seconds
|
|
1083
1112
|
(a positive integer; validated HERE, at decoration time — a
|
|
1084
1113
|
zero, negative, or non-integer value raises ``ValueError``
|
|
@@ -1094,13 +1123,15 @@ class App:
|
|
|
1094
1123
|
retries: Automatic re-attempts after a failure (default 0). All
|
|
1095
1124
|
attempts share one ``timeout`` budget.
|
|
1096
1125
|
**extra: Additional resource requests, recorded verbatim in the
|
|
1097
|
-
job's resource spec.
|
|
1098
|
-
``produces`` and ``
|
|
1099
|
-
else passes through to the resource spec unchanged.
|
|
1126
|
+
job's resource spec. Three names are RESERVED and rejected —
|
|
1127
|
+
``produces``, ``consumes`` and ``gpu`` (see Raises below);
|
|
1128
|
+
everything else passes through to the resource spec unchanged.
|
|
1100
1129
|
|
|
1101
1130
|
Raises:
|
|
1102
1131
|
ValueError: ``timeout`` is not ``None`` and is not a positive
|
|
1103
1132
|
integer. ``True`` and ``False`` are not accepted as timeouts.
|
|
1133
|
+
Also raised when ``system`` names a tier the platform cannot
|
|
1134
|
+
provision yet (``SYSTEM_SPECS[system].available`` is false).
|
|
1104
1135
|
TypeError: ``produces=`` or ``consumes=`` was passed. Outputs are
|
|
1105
1136
|
not declared on the decorator. Callbacks such as
|
|
1106
1137
|
``ResumableCheckpoint`` already declare their supported
|
|
@@ -1108,6 +1139,11 @@ class App:
|
|
|
1108
1139
|
volume for files another job needs. ``consumes=`` is reserved;
|
|
1109
1140
|
continue from a checkpoint with
|
|
1110
1141
|
``simulo run --from <job>[:latest|:best]`` instead.
|
|
1142
|
+
Also raised for ``gpu=`` (the free-text GPU request this
|
|
1143
|
+
parameter replaced; it would otherwise fall through
|
|
1144
|
+
``**extra`` into the resource spec unvalidated) and for a
|
|
1145
|
+
``system`` value that is not a :class:`simulo.SystemType`
|
|
1146
|
+
member.
|
|
1111
1147
|
"""
|
|
1112
1148
|
if timeout is not None and (isinstance(timeout, bool) or not isinstance(timeout, int) or timeout < 1):
|
|
1113
1149
|
raise ValueError(
|
|
@@ -1127,11 +1163,44 @@ class App:
|
|
|
1127
1163
|
"today. To chain jobs, run with `simulo run --from <job>[:latest|:best]`. Remove "
|
|
1128
1164
|
"consumes=."
|
|
1129
1165
|
)
|
|
1166
|
+
# ``gpu`` was a real named parameter until the system-tier catalog
|
|
1167
|
+
# replaced it (#1362). Removing it from the signature alone does NOT
|
|
1168
|
+
# reject ``gpu="L4"``: CPython routes the unknown keyword into
|
|
1169
|
+
# ``**extra`` and it would land in the resource spec exactly as before,
|
|
1170
|
+
# unvalidated. This guard is what makes the rename a rejection.
|
|
1171
|
+
if "gpu" in extra:
|
|
1172
|
+
raise TypeError(
|
|
1173
|
+
"@app.job(gpu=...) is no longer supported. Request a GPU system tier instead: "
|
|
1174
|
+
"@app.job(system=simulo.SystemType.TIER_1). Run `simulo systems` to list the "
|
|
1175
|
+
"tiers and which are available today. Remove gpu=."
|
|
1176
|
+
)
|
|
1177
|
+
if system is not None:
|
|
1178
|
+
# Static lookups against the published catalog — no fleet state
|
|
1179
|
+
# is consulted. A raw string is refused even when it spells a
|
|
1180
|
+
# valid wire value, so the one accepted spelling is the enum.
|
|
1181
|
+
if not isinstance(system, SystemType):
|
|
1182
|
+
raise TypeError(
|
|
1183
|
+
"@app.job(system=...) must be a simulo.SystemType member, e.g. "
|
|
1184
|
+
f"system=simulo.SystemType.TIER_1; got {system!r}. Run `simulo systems` to "
|
|
1185
|
+
"list the tiers."
|
|
1186
|
+
)
|
|
1187
|
+
spec = SYSTEM_SPECS[system]
|
|
1188
|
+
if not spec.available:
|
|
1189
|
+
available = ", ".join(
|
|
1190
|
+
f"SystemType.{tier.name} ({entry.gpu_model})"
|
|
1191
|
+
for tier, entry in SYSTEM_SPECS.items()
|
|
1192
|
+
if entry.available
|
|
1193
|
+
)
|
|
1194
|
+
raise ValueError(
|
|
1195
|
+
f"@app.job(system=SystemType.{system.name}) ({spec.gpu_model}) is not available on "
|
|
1196
|
+
f"the platform yet. Available today: {available}. Run `simulo systems` for the "
|
|
1197
|
+
"full catalog."
|
|
1198
|
+
)
|
|
1130
1199
|
|
|
1131
1200
|
def decorate(func: F) -> JobFunction:
|
|
1132
1201
|
resources: dict[str, Any] = {}
|
|
1133
|
-
if
|
|
1134
|
-
resources["
|
|
1202
|
+
if system is not None:
|
|
1203
|
+
resources["system"] = system.value
|
|
1135
1204
|
resources.update(extra)
|
|
1136
1205
|
spec = JobSpec(
|
|
1137
1206
|
name=func.__name__,
|
|
@@ -57,6 +57,14 @@ wheel's own bundled resources.
|
|
|
57
57
|
``./simulo-samples`` with normal Git history. It uses neither client settings nor
|
|
58
58
|
saved sign-in state; a failed clone is removed before it can appear at that path.
|
|
59
59
|
|
|
60
|
+
``simulo systems [--json]`` lists the published GPU system tiers (``SystemType`` /
|
|
61
|
+
``SYSTEM_SPECS`` from ``simulo-interfaces``) that ``@app.job(system=...)`` accepts —
|
|
62
|
+
table or ``--json``. This reads only that static, in-package catalog: no client
|
|
63
|
+
construction, no credential resolution, no network call. Today's fleet is one shared
|
|
64
|
+
machine, so the listing is descriptive rather than a live capability check — the
|
|
65
|
+
tier a job requests is recorded on the job, not scheduled against, and the table's
|
|
66
|
+
own footer says so.
|
|
67
|
+
|
|
60
68
|
**Auth** (``simulo login`` / ``logout`` / ``whoami``) drives the OAuth 2.0 +
|
|
61
69
|
PKCE loopback flow and manages
|
|
62
70
|
``~/.simulo/credentials``.
|
|
@@ -123,6 +131,7 @@ import json
|
|
|
123
131
|
import math
|
|
124
132
|
import os
|
|
125
133
|
import re
|
|
134
|
+
import shlex
|
|
126
135
|
import shutil
|
|
127
136
|
import socket
|
|
128
137
|
import sys
|
|
@@ -186,7 +195,12 @@ from simulo._client.discovery import discover, import_app_module
|
|
|
186
195
|
from simulo._client.export_api import DEFAULT_EXPORT_FORMAT, ExportApiClient, ExportApiError
|
|
187
196
|
from simulo._client.export_bundle import PolicyBundleError, extract_policy_bundle
|
|
188
197
|
from simulo._client.identity_api import IdentityApiClient, IdentityApiError
|
|
189
|
-
from simulo._client.install_samples import
|
|
198
|
+
from simulo._client.install_samples import (
|
|
199
|
+
DEFAULT_SAMPLES_DIRECTORY_NAME,
|
|
200
|
+
InstallSamplesError,
|
|
201
|
+
InstallSamplesUsageError,
|
|
202
|
+
install_samples,
|
|
203
|
+
)
|
|
190
204
|
from simulo._client.jobs_api import JobsApiClient, JobsApiError, JobsApiHTTPError, JobsApiUnavailable
|
|
191
205
|
from simulo._client.login import LoginError, perform_login
|
|
192
206
|
from simulo._client.mode import DISCOVERY
|
|
@@ -214,6 +228,7 @@ from simulo.interfaces.ids import (
|
|
|
214
228
|
parse_output_public_id,
|
|
215
229
|
parse_recording_public_id,
|
|
216
230
|
)
|
|
231
|
+
from simulo.interfaces.platform import SYSTEM_SPECS, SystemType
|
|
217
232
|
from simulo.interfaces.platform.asset_catalog import (
|
|
218
233
|
ASSET_ARCHIVE_TOO_LARGE_CODE,
|
|
219
234
|
ASSET_KINDS,
|
|
@@ -6982,6 +6997,77 @@ def _print_table(headers: tuple[str, ...], rows: Sequence[tuple[str, ...]]) -> N
|
|
|
6982
6997
|
print(" ".join(cell.ljust(width) for cell, width in zip(line, widths)).rstrip())
|
|
6983
6998
|
|
|
6984
6999
|
|
|
7000
|
+
#: One line, plain language, no jargon: forecloses the exact misreading issue
|
|
7001
|
+
#: #1512 documents in the wild (the samples repository's "no GPU requested"
|
|
7002
|
+
#: silently being read as "schedules faster" — untrue on a single-fleet
|
|
7003
|
+
#: platform). A numbered tier ladder invites the identical misreading in the
|
|
7004
|
+
#: other direction — "higher tier, faster/better scheduling" — for
|
|
7005
|
+
#: ``system=SystemType.TIER_4``, so this explicitly rules out BOTH readings,
|
|
7006
|
+
#: not just states the record-only fact and hopes the reader draws the right
|
|
7007
|
+
#: conclusion. Printed by the table view only: ``--json`` is a machine
|
|
7008
|
+
#: contract and never carries prose.
|
|
7009
|
+
_SYSTEMS_FOOTER = (
|
|
7010
|
+
"Your `system=` choice is recorded on the job. It does not select your hardware today: "
|
|
7011
|
+
"no tier schedules faster or trains better than another, and neither does leaving "
|
|
7012
|
+
"`system=` out entirely."
|
|
7013
|
+
)
|
|
7014
|
+
|
|
7015
|
+
|
|
7016
|
+
def _system_tier_label(system: SystemType) -> str:
|
|
7017
|
+
"""The human-facing tier label for the table view, e.g. ``SystemType.TIER_1`` ->
|
|
7018
|
+
``"Tier 1 (tier1)"``. The parenthesized wire value is the exact string
|
|
7019
|
+
``system=`` accepts (what ``--json`` prints under the ``"system"`` key);
|
|
7020
|
+
the table would otherwise show a reader nothing they could type."""
|
|
7021
|
+
return f"{system.name.replace('_', ' ').title()} ({system.value})"
|
|
7022
|
+
|
|
7023
|
+
|
|
7024
|
+
def _cmd_systems(args: argparse.Namespace) -> int:
|
|
7025
|
+
"""``simulo systems [--json]`` — list the published GPU system tiers.
|
|
7026
|
+
|
|
7027
|
+
Reads only the static, in-package ``SYSTEM_SPECS`` catalog from
|
|
7028
|
+
``simulo-interfaces``: no client construction, no credential resolution,
|
|
7029
|
+
no network call of any kind. Always lists all four tiers, including the
|
|
7030
|
+
two not yet available — the point is to show what exists and what the
|
|
7031
|
+
fleet can provision TODAY, not to hide the rest.
|
|
7032
|
+
|
|
7033
|
+
The raw AWS SKU behind a tier never appears in either output — the whole
|
|
7034
|
+
reason this catalog names abstract tiers instead of a hardware SKU (see
|
|
7035
|
+
``SystemSpec``'s own docstring for the field that stays off every
|
|
7036
|
+
user-facing surface, including this one).
|
|
7037
|
+
"""
|
|
7038
|
+
if args.json:
|
|
7039
|
+
records = [
|
|
7040
|
+
{
|
|
7041
|
+
"system": system.value,
|
|
7042
|
+
"gpu_model": spec.gpu_model,
|
|
7043
|
+
"vram_gb": spec.vram_gb,
|
|
7044
|
+
"vcpu": spec.vcpu,
|
|
7045
|
+
"memory_gb": spec.memory_gb,
|
|
7046
|
+
"max_parallel_envs": spec.max_parallel_envs,
|
|
7047
|
+
"available": spec.available,
|
|
7048
|
+
}
|
|
7049
|
+
for system, spec in SYSTEM_SPECS.items()
|
|
7050
|
+
]
|
|
7051
|
+
print(json.dumps(records, indent=2))
|
|
7052
|
+
return 0
|
|
7053
|
+
|
|
7054
|
+
rows = [
|
|
7055
|
+
(
|
|
7056
|
+
_system_tier_label(system),
|
|
7057
|
+
spec.gpu_model,
|
|
7058
|
+
f"{spec.vram_gb} GB",
|
|
7059
|
+
str(spec.vcpu),
|
|
7060
|
+
f"{spec.memory_gb} GB",
|
|
7061
|
+
str(spec.max_parallel_envs) if spec.max_parallel_envs is not None else "not yet measured",
|
|
7062
|
+
"available" if spec.available else "not yet available",
|
|
7063
|
+
)
|
|
7064
|
+
for system, spec in SYSTEM_SPECS.items()
|
|
7065
|
+
]
|
|
7066
|
+
_print_table(("TIER", "GPU MODEL", "VRAM", "VCPU", "MEMORY", "MAX PARALLEL ENVS", "STATUS"), rows)
|
|
7067
|
+
print(_SYSTEMS_FOOTER)
|
|
7068
|
+
return 0
|
|
7069
|
+
|
|
7070
|
+
|
|
6985
7071
|
#: The frozen local-development cache root for ``simulo asset get``:
|
|
6986
7072
|
#: ``~/.simulo/assets/<org>/<kind>/<name>/v<N>/`` — colon-free segments,
|
|
6987
7073
|
#: NTFS-safe. Mirrors ``credentials.CREDENTIALS_PATH``'s
|
|
@@ -8013,7 +8099,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
8013
8099
|
prog="simulo",
|
|
8014
8100
|
description=(
|
|
8015
8101
|
"Simulo thin client — submit an app to the Simulo cloud and observe its runs "
|
|
8016
|
-
"(simulo jobs / logs / result / recordings / models / outputs / export / view / cancel)."
|
|
8102
|
+
"(simulo systems / jobs / logs / result / recordings / models / outputs / export / view / cancel)."
|
|
8017
8103
|
),
|
|
8018
8104
|
)
|
|
8019
8105
|
# ``action="version"`` fires (prints + exits 0) while argparse is still
|
|
@@ -8269,14 +8355,33 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
8269
8355
|
install_sub = install.add_subparsers(dest="install_command", required=True)
|
|
8270
8356
|
samples = install_sub.add_parser(
|
|
8271
8357
|
"samples",
|
|
8272
|
-
help="Clone the sample collection into ./simulo-samples with full Git history.",
|
|
8358
|
+
help="Clone the sample collection into ./simulo-samples by default with full Git history.",
|
|
8359
|
+
)
|
|
8360
|
+
samples.add_argument(
|
|
8361
|
+
"--directory",
|
|
8362
|
+
metavar="PATH",
|
|
8363
|
+
type=Path,
|
|
8364
|
+
help="Install into PATH instead of ./simulo-samples. PATH's parent directory must already exist.",
|
|
8365
|
+
)
|
|
8366
|
+
samples.add_argument(
|
|
8367
|
+
"--ref",
|
|
8368
|
+
metavar="REF",
|
|
8369
|
+
help="Check out Git branch, tag, or commit REF after cloning.",
|
|
8273
8370
|
)
|
|
8274
8371
|
|
|
8275
8372
|
def _handle_install_samples(args: argparse.Namespace, extras: Sequence[str]) -> int:
|
|
8276
8373
|
if extras:
|
|
8277
8374
|
samples.error(f"unrecognized arguments: {' '.join(extras)}")
|
|
8375
|
+
directory = args.directory
|
|
8376
|
+
if directory is None:
|
|
8377
|
+
parent_directory = Path.cwd()
|
|
8378
|
+
directory_name = DEFAULT_SAMPLES_DIRECTORY_NAME
|
|
8379
|
+
else:
|
|
8380
|
+
target = directory if directory.is_absolute() else Path.cwd() / directory
|
|
8381
|
+
parent_directory = target.parent
|
|
8382
|
+
directory_name = target.name
|
|
8278
8383
|
try:
|
|
8279
|
-
installed = install_samples(
|
|
8384
|
+
installed = install_samples(parent_directory, directory_name=directory_name, ref=args.ref)
|
|
8280
8385
|
except InstallSamplesUsageError as exc:
|
|
8281
8386
|
print(f"error: {exc}", file=sys.stderr)
|
|
8282
8387
|
print("Nothing was installed.", file=sys.stderr)
|
|
@@ -8285,12 +8390,21 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
8285
8390
|
print(f"error: {exc}", file=sys.stderr)
|
|
8286
8391
|
print("Nothing was installed.", file=sys.stderr)
|
|
8287
8392
|
return 1
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8393
|
+
if args.ref is None:
|
|
8394
|
+
print(
|
|
8395
|
+
f"Installed the Simulo samples in {installed.path} "
|
|
8396
|
+
"(a normal Git clone: history, branches, and git pull all work)."
|
|
8397
|
+
)
|
|
8398
|
+
else:
|
|
8399
|
+
print(
|
|
8400
|
+
f"Installed the Simulo samples in {installed.path} "
|
|
8401
|
+
f"(a normal Git clone pinned to {args.ref!r}; the checkout is detached)."
|
|
8402
|
+
)
|
|
8292
8403
|
print("Next steps:")
|
|
8293
|
-
|
|
8404
|
+
if directory is None:
|
|
8405
|
+
print(" cd simulo-samples")
|
|
8406
|
+
else:
|
|
8407
|
+
print(f" cd {shlex.quote(str(installed.path))}")
|
|
8294
8408
|
print(" cat README.md")
|
|
8295
8409
|
print("README.md is the sample index; each sample folder has its own app.py to run with simulo run.")
|
|
8296
8410
|
return 0
|
|
@@ -8757,6 +8871,20 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
8757
8871
|
)
|
|
8758
8872
|
models.set_defaults(func=_observe_handler(models, _cmd_models))
|
|
8759
8873
|
|
|
8874
|
+
systems = sub.add_parser(
|
|
8875
|
+
"systems",
|
|
8876
|
+
help=(
|
|
8877
|
+
"List the published GPU system tiers `@app.job(system=...)` accepts — table or "
|
|
8878
|
+
"--json. Reads only the in-package catalog: no network call."
|
|
8879
|
+
),
|
|
8880
|
+
)
|
|
8881
|
+
systems.add_argument(
|
|
8882
|
+
"--json",
|
|
8883
|
+
action="store_true",
|
|
8884
|
+
help="Print the tier catalog as a JSON array instead of a table (automation-friendly).",
|
|
8885
|
+
)
|
|
8886
|
+
systems.set_defaults(func=_observe_handler(systems, _cmd_systems))
|
|
8887
|
+
|
|
8760
8888
|
# `simulo export` — one command, no arguments, on the happy path. Every
|
|
8761
8889
|
# option below overrides a default that was already correct (#654): the
|
|
8762
8890
|
# latest job, its best checkpoint, ONNX, download, unpack. A new REQUIRED
|
|
@@ -48,7 +48,7 @@ class HttpError(RuntimeError):
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
class HttpUnavailable(HttpError):
|
|
51
|
-
"""The endpoint
|
|
51
|
+
"""The endpoint is unreachable or its response times out."""
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
class HttpHTTPError(HttpError):
|
|
@@ -373,6 +373,8 @@ def request_bytes(
|
|
|
373
373
|
raise _structured_http_error(exc) from exc
|
|
374
374
|
except urllib.error.URLError as exc:
|
|
375
375
|
raise HttpUnavailable(_unavailable_message(url, unavailable_hint, exc.reason)) from exc
|
|
376
|
+
except TimeoutError as exc:
|
|
377
|
+
raise HttpUnavailable(_unavailable_message(url, unavailable_hint, exc)) from exc
|
|
376
378
|
|
|
377
379
|
|
|
378
380
|
def request_json(
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
import shutil
|
|
7
8
|
import subprocess
|
|
8
9
|
import sys
|
|
@@ -134,6 +135,8 @@ def _remove_staging_directory(staging: Path, identity: tuple[int, int]) -> None:
|
|
|
134
135
|
print(f"warning: partial samples install path no longer belongs to this invocation: {staging}", file=sys.stderr)
|
|
135
136
|
return
|
|
136
137
|
try:
|
|
138
|
+
if not os.path.islink(staging):
|
|
139
|
+
os.chmod(staging, 0o700)
|
|
137
140
|
shutil.rmtree(staging, onerror=_remove_read_only_path)
|
|
138
141
|
except OSError:
|
|
139
142
|
print(f"warning: could not remove partial samples install at {staging}", file=sys.stderr)
|
|
@@ -160,6 +163,11 @@ def _target_exists_error(target: Path) -> InstallSamplesUsageError:
|
|
|
160
163
|
return InstallSamplesUsageError(f"{target} already exists.")
|
|
161
164
|
|
|
162
165
|
|
|
166
|
+
def _path_exists(path: Path) -> bool:
|
|
167
|
+
"""Return whether *path* exists, including a dangling symbolic link."""
|
|
168
|
+
return os.path.lexists(path)
|
|
169
|
+
|
|
170
|
+
|
|
163
171
|
def _git_is_reachable_through_working_directory(
|
|
164
172
|
named_git_path: Path, git_executable: Path, working_directory: Path
|
|
165
173
|
) -> bool:
|
|
@@ -190,11 +198,14 @@ def install_samples(
|
|
|
190
198
|
*,
|
|
191
199
|
repository_url: str = SAMPLES_REPOSITORY_URL,
|
|
192
200
|
directory_name: str = DEFAULT_SAMPLES_DIRECTORY_NAME,
|
|
201
|
+
ref: str | None = None,
|
|
193
202
|
timeout_s: int = CLONE_TIMEOUT_S,
|
|
194
203
|
) -> InstalledSamples:
|
|
195
204
|
"""Clone samples into *parent_directory* and reveal them atomically."""
|
|
196
205
|
target = parent_directory / directory_name
|
|
197
|
-
if
|
|
206
|
+
if not parent_directory.is_dir():
|
|
207
|
+
raise InstallSamplesUsageError(f"Parent directory {parent_directory} does not exist or is not a directory.")
|
|
208
|
+
if _path_exists(target):
|
|
198
209
|
raise _target_exists_error(target)
|
|
199
210
|
|
|
200
211
|
git_path = shutil.which("git")
|
|
@@ -223,7 +234,10 @@ def install_samples(
|
|
|
223
234
|
staging_identity = _directory_identity(staging)
|
|
224
235
|
|
|
225
236
|
print("Cloning the samples with full history; this can take a minute.", file=sys.stderr)
|
|
226
|
-
argv = [str(git_executable), "clone"
|
|
237
|
+
argv = [str(git_executable), "clone"]
|
|
238
|
+
if ref is not None:
|
|
239
|
+
argv.append("--no-checkout")
|
|
240
|
+
argv.extend(["--", repository_url, str(staging)])
|
|
227
241
|
try:
|
|
228
242
|
completed = _run_git(argv, timeout_s=timeout_s, env=_git_environment())
|
|
229
243
|
except subprocess.TimeoutExpired as exc:
|
|
@@ -249,11 +263,131 @@ def install_samples(
|
|
|
249
263
|
explain_public_repository=True,
|
|
250
264
|
)
|
|
251
265
|
|
|
252
|
-
if not _clone_has_sample_files(staging):
|
|
266
|
+
if ref is None and not _clone_has_sample_files(staging):
|
|
253
267
|
_remove_staging_directory(staging, staging_identity)
|
|
254
268
|
raise _clone_failure("Git clone produced an empty working tree.", completed.stderr)
|
|
255
269
|
|
|
256
|
-
if
|
|
270
|
+
if ref is not None:
|
|
271
|
+
try:
|
|
272
|
+
resolved = _run_git(
|
|
273
|
+
[
|
|
274
|
+
str(git_executable),
|
|
275
|
+
"-C",
|
|
276
|
+
str(staging),
|
|
277
|
+
"rev-parse",
|
|
278
|
+
"--verify",
|
|
279
|
+
"--end-of-options",
|
|
280
|
+
f"{ref}^{{commit}}",
|
|
281
|
+
],
|
|
282
|
+
timeout_s=timeout_s,
|
|
283
|
+
env=_git_environment(),
|
|
284
|
+
)
|
|
285
|
+
except subprocess.TimeoutExpired as exc:
|
|
286
|
+
_remove_staging_directory(staging, staging_identity)
|
|
287
|
+
raise _clone_failure(f"Git could not resolve samples reference {ref!r}: timed out.", exc.stderr) from None
|
|
288
|
+
except KeyboardInterrupt:
|
|
289
|
+
_remove_staging_directory(staging, staging_identity)
|
|
290
|
+
raise
|
|
291
|
+
except OSError as exc:
|
|
292
|
+
_remove_staging_directory(staging, staging_identity)
|
|
293
|
+
reason = exc.strerror or str(exc)
|
|
294
|
+
raise InstallSamplesError(f"Could not resolve samples reference {ref!r}: {reason}.") from None
|
|
295
|
+
if resolved.returncode != 0:
|
|
296
|
+
try:
|
|
297
|
+
resolved = _run_git(
|
|
298
|
+
[
|
|
299
|
+
str(git_executable),
|
|
300
|
+
"-C",
|
|
301
|
+
str(staging),
|
|
302
|
+
"rev-parse",
|
|
303
|
+
"--verify",
|
|
304
|
+
"--end-of-options",
|
|
305
|
+
f"refs/remotes/origin/{ref}^{{commit}}",
|
|
306
|
+
],
|
|
307
|
+
timeout_s=timeout_s,
|
|
308
|
+
env=_git_environment(),
|
|
309
|
+
)
|
|
310
|
+
except subprocess.TimeoutExpired as exc:
|
|
311
|
+
_remove_staging_directory(staging, staging_identity)
|
|
312
|
+
raise _clone_failure(
|
|
313
|
+
f"Git could not resolve samples reference {ref!r}: timed out.", exc.stderr
|
|
314
|
+
) from None
|
|
315
|
+
except KeyboardInterrupt:
|
|
316
|
+
_remove_staging_directory(staging, staging_identity)
|
|
317
|
+
raise
|
|
318
|
+
except OSError as exc:
|
|
319
|
+
_remove_staging_directory(staging, staging_identity)
|
|
320
|
+
reason = exc.strerror or str(exc)
|
|
321
|
+
raise InstallSamplesError(f"Could not resolve samples reference {ref!r}: {reason}.") from None
|
|
322
|
+
if resolved.returncode != 0:
|
|
323
|
+
_remove_staging_directory(staging, staging_identity)
|
|
324
|
+
raise _clone_failure(f"Git could not resolve samples reference {ref!r}.", resolved.stderr)
|
|
325
|
+
commit = resolved.stdout.strip()
|
|
326
|
+
if re.fullmatch(r"[0-9a-fA-F]{40,64}", commit) is None:
|
|
327
|
+
_remove_staging_directory(staging, staging_identity)
|
|
328
|
+
raise InstallSamplesError(f"Git could not resolve samples reference {ref!r} to a commit.")
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
checked_out = _run_git(
|
|
332
|
+
[str(git_executable), "-C", str(staging), "checkout", "--detach", commit],
|
|
333
|
+
timeout_s=timeout_s,
|
|
334
|
+
env=_git_environment(),
|
|
335
|
+
)
|
|
336
|
+
except subprocess.TimeoutExpired as exc:
|
|
337
|
+
_remove_staging_directory(staging, staging_identity)
|
|
338
|
+
raise _clone_failure(f"Git could not check out samples reference {ref!r}: timed out.", exc.stderr) from None
|
|
339
|
+
except KeyboardInterrupt:
|
|
340
|
+
_remove_staging_directory(staging, staging_identity)
|
|
341
|
+
raise
|
|
342
|
+
except OSError as exc:
|
|
343
|
+
_remove_staging_directory(staging, staging_identity)
|
|
344
|
+
reason = exc.strerror or str(exc)
|
|
345
|
+
raise InstallSamplesError(f"Could not check out samples reference {ref!r}: {reason}.") from None
|
|
346
|
+
if checked_out.returncode != 0:
|
|
347
|
+
_remove_staging_directory(staging, staging_identity)
|
|
348
|
+
raise _clone_failure(f"Git could not check out samples reference {ref!r}.", checked_out.stderr)
|
|
349
|
+
if not _clone_has_sample_files(staging):
|
|
350
|
+
_remove_staging_directory(staging, staging_identity)
|
|
351
|
+
raise _clone_failure("Git checkout produced an empty working tree.", checked_out.stderr)
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
head = _run_git(
|
|
355
|
+
[str(git_executable), "-C", str(staging), "rev-parse", "--verify", "HEAD"],
|
|
356
|
+
timeout_s=timeout_s,
|
|
357
|
+
env=_git_environment(),
|
|
358
|
+
)
|
|
359
|
+
worktree = _run_git(
|
|
360
|
+
[
|
|
361
|
+
str(git_executable),
|
|
362
|
+
"-C",
|
|
363
|
+
str(staging),
|
|
364
|
+
"diff",
|
|
365
|
+
"--quiet",
|
|
366
|
+
commit,
|
|
367
|
+
"--",
|
|
368
|
+
],
|
|
369
|
+
timeout_s=timeout_s,
|
|
370
|
+
env=_git_environment(),
|
|
371
|
+
)
|
|
372
|
+
except subprocess.TimeoutExpired as exc:
|
|
373
|
+
_remove_staging_directory(staging, staging_identity)
|
|
374
|
+
raise _clone_failure(f"Git could not verify samples reference {ref!r}: timed out.", exc.stderr) from None
|
|
375
|
+
except KeyboardInterrupt:
|
|
376
|
+
_remove_staging_directory(staging, staging_identity)
|
|
377
|
+
raise
|
|
378
|
+
except OSError as exc:
|
|
379
|
+
_remove_staging_directory(staging, staging_identity)
|
|
380
|
+
reason = exc.strerror or str(exc)
|
|
381
|
+
raise InstallSamplesError(f"Could not verify samples reference {ref!r}: {reason}.") from None
|
|
382
|
+
if head.returncode != 0 or worktree.returncode not in (0, 1):
|
|
383
|
+
_remove_staging_directory(staging, staging_identity)
|
|
384
|
+
stderr = head.stderr if head.returncode != 0 else worktree.stderr
|
|
385
|
+
raise _clone_failure(f"Git could not verify samples reference {ref!r}.", stderr)
|
|
386
|
+
if head.stdout.strip().lower() != commit.lower() or worktree.returncode != 0:
|
|
387
|
+
_remove_staging_directory(staging, staging_identity)
|
|
388
|
+
raise InstallSamplesError("Git checkout did not leave the requested samples revision clean.")
|
|
389
|
+
|
|
390
|
+
if _path_exists(target):
|
|
257
391
|
_remove_staging_directory(staging, staging_identity)
|
|
258
392
|
raise _target_exists_error(target)
|
|
259
393
|
|
|
@@ -17,18 +17,10 @@ always returns a :class:`PreflightOutcome`, either a genuine
|
|
|
17
17
|
for the CLI to print verbatim before proceeding. Only a genuinely
|
|
18
18
|
well-formed 200 response can carry a ``blocking`` finding.
|
|
19
19
|
|
|
20
|
-
The
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
but a peer that accepts the connection and then simply never answers (this
|
|
25
|
-
PR's own connection-timeout test reproduces it) times out INSIDE
|
|
26
|
-
``http.client.HTTPConnection.getresponse()`` — a call site ``http.py``'s
|
|
27
|
-
existing ``except urllib.error.URLError`` does not wrap, so a bare
|
|
28
|
-
``socket.timeout``/``TimeoutError`` can reach this call site unwrapped. The
|
|
29
|
-
broad catch is the safety net for exactly that gap (and for anything else
|
|
30
|
-
unanticipated) — preflight must not add a second instance of this failure class to the
|
|
31
|
-
submit path.
|
|
20
|
+
The shared HTTP helper wraps connection failures and response timeouts as
|
|
21
|
+
``HttpUnavailable``. The final ``except Exception`` in
|
|
22
|
+
:meth:`PreflightApiClient.evaluate` preserves the fail-open contract for other
|
|
23
|
+
unexpected errors: only a well-formed findings response may block submission.
|
|
32
24
|
"""
|
|
33
25
|
|
|
34
26
|
from __future__ import annotations
|
|
@@ -420,6 +420,7 @@ class SubmitApiClient:
|
|
|
420
420
|
seed_from_job_id: Optional[str] = None,
|
|
421
421
|
assets: Optional[list[dict[str, str]]] = None,
|
|
422
422
|
app_name: Optional[str] = None,
|
|
423
|
+
system: Optional[str] = None,
|
|
423
424
|
) -> dict[str, Any]:
|
|
424
425
|
"""``POST /v1/jobs`` — create the job that executes *package_id*.
|
|
425
426
|
|
|
@@ -441,6 +442,14 @@ class SubmitApiClient:
|
|
|
441
442
|
predates the live-visualization plan's ``JobSubmitRequest`` field
|
|
442
443
|
simply ignores an absent key, same as it ignores any unknown field.
|
|
443
444
|
|
|
445
|
+
``system`` (system-tier-catalog-1362, M50-persistence) is the
|
|
446
|
+
already-validated tier wire value (e.g. ``"tier1"``) from the job's
|
|
447
|
+
own manifest ``resources["system"]`` — an OPTIONAL top-level sibling
|
|
448
|
+
of ``args`` for the same package-hash-invariance reason as
|
|
449
|
+
``viewstream`` above. Sent only when truthy: a control plane that
|
|
450
|
+
predates this field ignores the extra key, and the run proceeds
|
|
451
|
+
exactly as it did before ``system=`` existed.
|
|
452
|
+
|
|
444
453
|
``seed_from_job_id`` (``simulo run --from``, explicit-run-intent
|
|
445
454
|
plan) follows the exact same sibling-of-``args`` rule for the exact
|
|
446
455
|
same package-hash reason — a seed is a property of THIS submission,
|
|
@@ -500,6 +509,8 @@ class SubmitApiClient:
|
|
|
500
509
|
body["app_name"] = app_name
|
|
501
510
|
if viewstream:
|
|
502
511
|
body["viewstream"] = True
|
|
512
|
+
if system:
|
|
513
|
+
body["system"] = system
|
|
503
514
|
if seed_from_job_id is not None:
|
|
504
515
|
body[_SEED_FROM_JOB_ID_FIELD] = seed_from_job_id
|
|
505
516
|
if assets:
|
|
@@ -209,8 +209,9 @@ def _make_env(num_envs: int) -> Any:
|
|
|
209
209
|
)
|
|
210
210
|
|
|
211
211
|
|
|
212
|
+
# system= picks a GPU tier from the published catalog; run `simulo systems` to see them.
|
|
212
213
|
@app.job(
|
|
213
|
-
|
|
214
|
+
system=simulo.SystemType.TIER_1,
|
|
214
215
|
timeout=8 * 60 * 60,
|
|
215
216
|
retries=2,
|
|
216
217
|
callbacks=[simulo.callbacks.ResumableCheckpoint(every=50)],
|
|
@@ -239,7 +240,7 @@ def train(num_envs: int = 512, max_iterations: int = 20) -> dict[str, Any]:
|
|
|
239
240
|
return {"checkpoint": checkpoint, "policy": policy_path, "num_envs": num_envs, **stats}
|
|
240
241
|
|
|
241
242
|
|
|
242
|
-
@app.job(
|
|
243
|
+
@app.job(system=simulo.SystemType.TIER_1, timeout=2 * 60 * 60)
|
|
243
244
|
def evaluate(num_episodes: int = 10, num_rounds: int = 5) -> dict[str, Any]:
|
|
244
245
|
"""Evaluate the saved checkpoint over several rounds; aggregate with numpy."""
|
|
245
246
|
import json
|
|
@@ -283,7 +284,7 @@ def evaluate(num_episodes: int = 10, num_rounds: int = 5) -> dict[str, Any]:
|
|
|
283
284
|
return {"report": report_path, **report}
|
|
284
285
|
|
|
285
286
|
|
|
286
|
-
@app.job(
|
|
287
|
+
@app.job(system=simulo.SystemType.TIER_1, timeout=1 * 60 * 60)
|
|
287
288
|
def rollout(num_steps: int = 200) -> dict[str, Any]:
|
|
288
289
|
"""Inference path: play the exported TorchScript policy and record it to MCAP.
|
|
289
290
|
|
|
@@ -64,7 +64,8 @@ class {{APP_NAME}}Scenario(simulo.Scenario): # -> ScenarioProtocol at submit, r
|
|
|
64
64
|
app = simulo.App("{{APP_NAME}}")
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
# system= picks a GPU tier from the published catalog; run `simulo systems` to see them.
|
|
68
|
+
@app.job(system=simulo.SystemType.TIER_1, timeout=30 * 60)
|
|
68
69
|
def simulate(num_steps: int = 1000, num_envs: int = 4) -> dict[str, Any]:
|
|
69
70
|
"""Run the scenario headless on the worker. EDIT: device / step count / env count.
|
|
70
71
|
|
|
@@ -193,8 +193,9 @@ class {{APP_NAME}}Task(simulo.Task):
|
|
|
193
193
|
# Retries are safe: ResumableCheckpoint declares periodic checkpoints (every 50
|
|
194
194
|
# iterations) and resume defaults to "auto", so a retried / preempted run resumes
|
|
195
195
|
# from the latest checkpoint instead of restarting.
|
|
196
|
+
# system= picks a GPU tier from the published catalog; run `simulo systems` to see them.
|
|
196
197
|
@app.job(
|
|
197
|
-
|
|
198
|
+
system=simulo.SystemType.TIER_1,
|
|
198
199
|
timeout=8 * 60 * 60,
|
|
199
200
|
retries=2,
|
|
200
201
|
callbacks=[simulo.callbacks.ResumableCheckpoint(every=50)],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simulo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.27.0
|
|
4
4
|
Summary: Simulo SDK and CLI — define robotics simulation and training apps in Python and run them on the Simulo cloud.
|
|
5
5
|
Author-email: Simulo Team <team@simulo.ai>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Typing :: Typed
|
|
16
16
|
Requires-Python: >=3.11
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
-
Requires-Dist: simulo-interfaces<0.
|
|
18
|
+
Requires-Dist: simulo-interfaces<0.20,>=0.19.0
|
|
19
19
|
Requires-Dist: mcap<2,>=1.3
|
|
20
20
|
Requires-Dist: defusedxml>=0.7.1
|
|
21
21
|
Requires-Dist: usd-core<27,>=26.5
|
|
@@ -75,6 +75,19 @@ To download a curated collection of runnable projects, use `simulo install sampl
|
|
|
75
75
|
It creates `./simulo-samples` as a normal Git clone, so `cd simulo-samples` and
|
|
76
76
|
`cat README.md` open the sample index.
|
|
77
77
|
|
|
78
|
+
Choose a destination with `--directory` and a Git branch, tag, or commit with
|
|
79
|
+
`--ref`. You can use either option on its own or combine them:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
simulo install samples --directory robotics-examples --ref main
|
|
83
|
+
cd robotics-examples
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The destination must not exist, and its parent directory must already exist.
|
|
87
|
+
The directory appears only after the clone and requested checkout succeed.
|
|
88
|
+
Use a tag or full commit ID instead of `main` to select a fixed revision.
|
|
89
|
+
With `--ref`, the checkout is detached at the selected commit.
|
|
90
|
+
|
|
78
91
|
### Or write an app by hand
|
|
79
92
|
|
|
80
93
|
```python
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|