simulo 0.34.0__tar.gz → 0.35.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.34.0/src/simulo.egg-info → simulo-0.35.0}/PKG-INFO +5 -5
- {simulo-0.34.0 → simulo-0.35.0}/PYPI.md +1 -1
- {simulo-0.34.0 → simulo-0.35.0}/pyproject.toml +6 -5
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/__init__.py +6 -1
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/_runner.py +15 -1
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/app.py +53 -60
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/asset.py +2 -2
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/asset_pins.py +21 -2
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli.py +10 -7
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_rendering.py +10 -0
- simulo-0.35.0/src/simulo/_client/cli_runtime_limit.py +130 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/recordings.py +43 -12
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/run.py +88 -26
- simulo-0.35.0/src/simulo/_client/commands/systems.py +93 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/credentials.py +6 -1
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/http.py +66 -31
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/jobs_api.py +6 -1
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/login.py +20 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/preflight_api.py +9 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/preview_report.py +7 -3
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/submit_api.py +16 -1
- simulo-0.35.0/src/simulo/_client/system_requests.py +82 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/training/eval.py.tmpl +1 -1
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/training/play.py.tmpl +1 -1
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/training/preview.py.tmpl +1 -1
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/training/train.py.tmpl +1 -1
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/viewstream_policy.py +21 -15
- {simulo-0.34.0 → simulo-0.35.0/src/simulo.egg-info}/PKG-INFO +5 -5
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo.egg-info/SOURCES.txt +2 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo.egg-info/requires.txt +2 -2
- simulo-0.34.0/src/simulo/_client/commands/systems.py +0 -132
- {simulo-0.34.0 → simulo-0.35.0}/MANIFEST.in +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/setup.cfg +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/__init__.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/_arguments.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/_mounts.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/_secure_downloads.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/asset_api.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/asset_package.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/builtin_aliases.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/bundle.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cancel_api.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_arguments.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_context.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_downloads.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_errors.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_job_reporting.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_listing.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_text.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_waiting.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/__init__.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/assets.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/auth.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/create.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/jobs.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/policies.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/config.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/discovery.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/evaluation_report.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/export_api.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/facades.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/identity_api.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/install_samples.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/learning.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/mode.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/onnx_bundle.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/outputs.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/packaging.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/policies_api.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/preflight_render.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/recording_export.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/registry.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/repeatable_episodes.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/runtime.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/runtime_display.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/stub.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/__init__.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/training/simuloignore.tmpl +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/training/task.py.tmpl +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/training_algorithm.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/view_fragment.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/view_session_api.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/volume.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/callbacks.py +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo/py.typed +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo.egg-info/dependency_links.txt +0 -0
- {simulo-0.34.0 → simulo-0.35.0}/src/simulo.egg-info/entry_points.txt +0 -0
- {simulo-0.34.0 → simulo-0.35.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.35.0
|
|
4
4
|
Summary: Python SDK and CLI for cloud-native robotics simulation, reinforcement learning, and robot policy training on managed GPUs.
|
|
5
5
|
Author-email: Simulo Team <team@simulo.ai>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://docs.simulo.ai
|
|
|
9
9
|
Project-URL: Quickstart, https://docs.simulo.ai/getting-started/quickstart/
|
|
10
10
|
Project-URL: Examples, https://github.com/simulo-org/simulo-samples
|
|
11
11
|
Project-URL: Console, https://console.simulo.ai
|
|
12
|
-
Keywords: simulo,robotics,robotics simulation,robot training,robot learning,reinforcement learning,cloud robotics,cloud simulation,GPU simulation,policy training,physical AI,embodied AI,
|
|
12
|
+
Keywords: simulo,robotics,robotics simulation,robot training,robot learning,reinforcement learning,cloud robotics,cloud simulation,GPU simulation,policy training,physical AI,embodied AI,OpenUSD,USD,URDF,ONNX,MCAP,Python SDK
|
|
13
13
|
Classifier: Development Status :: 3 - Alpha
|
|
14
14
|
Classifier: Environment :: Console
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
@@ -31,7 +31,7 @@ Classifier: Topic :: System :: Distributed Computing
|
|
|
31
31
|
Classifier: Typing :: Typed
|
|
32
32
|
Requires-Python: >=3.11
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
34
|
-
Requires-Dist: simulo-interfaces<0.
|
|
34
|
+
Requires-Dist: simulo-interfaces<0.27,>=0.26.0
|
|
35
35
|
Requires-Dist: mcap<2,>=1.3
|
|
36
36
|
Requires-Dist: defusedxml>=0.7.1
|
|
37
37
|
Requires-Dist: usd-core<27,>=26.5
|
|
@@ -40,7 +40,7 @@ Requires-Dist: simulo-viz<0.2,>=0.1.0; python_version >= "3.12" and extra == "vi
|
|
|
40
40
|
Provides-Extra: assets
|
|
41
41
|
Requires-Dist: usd-core<27,>=26.5; extra == "assets"
|
|
42
42
|
Provides-Extra: dev
|
|
43
|
-
Requires-Dist: black==26.
|
|
43
|
+
Requires-Dist: black==26.10.1; extra == "dev"
|
|
44
44
|
Requires-Dist: isort==8.0.1; extra == "dev"
|
|
45
45
|
Requires-Dist: ruff==0.15.21; extra == "dev"
|
|
46
46
|
Requires-Dist: mypy==2.3.0; extra == "dev"
|
|
@@ -124,7 +124,7 @@ simulo policy get policy_x:best # download a verified checkpoint
|
|
|
124
124
|
simulo export policy_x:best # export a verified ONNX bundle
|
|
125
125
|
simulo recordings # download MCAP rollout recordings
|
|
126
126
|
simulo outputs # retrieve other saved outputs
|
|
127
|
-
simulo view # view a running
|
|
127
|
+
simulo view # view a running job in 3D (live view is on by default)
|
|
128
128
|
simulo cancel # stop a queued or running job
|
|
129
129
|
```
|
|
130
130
|
|
|
@@ -66,7 +66,7 @@ simulo policy get policy_x:best # download a verified checkpoint
|
|
|
66
66
|
simulo export policy_x:best # export a verified ONNX bundle
|
|
67
67
|
simulo recordings # download MCAP rollout recordings
|
|
68
68
|
simulo outputs # retrieve other saved outputs
|
|
69
|
-
simulo view # view a running
|
|
69
|
+
simulo view # view a running job in 3D (live view is on by default)
|
|
70
70
|
simulo cancel # stop a queued or running job
|
|
71
71
|
```
|
|
72
72
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "simulo"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.35.0"
|
|
8
8
|
description = "Python SDK and CLI for cloud-native robotics simulation, reinforcement learning, and robot policy training on managed GPUs."
|
|
9
9
|
readme = "PYPI.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -23,8 +23,6 @@ keywords = [
|
|
|
23
23
|
"policy training",
|
|
24
24
|
"physical AI",
|
|
25
25
|
"embodied AI",
|
|
26
|
-
"NVIDIA Isaac Sim",
|
|
27
|
-
"Isaac Lab",
|
|
28
26
|
"OpenUSD",
|
|
29
27
|
"USD",
|
|
30
28
|
"URDF",
|
|
@@ -59,7 +57,7 @@ classifiers = [
|
|
|
59
57
|
# mcap: used by `simulo recordings` to read back a downloaded recording and
|
|
60
58
|
# verify it (message count) — MIT-licensed, deps only lz4+zstandard.
|
|
61
59
|
dependencies = [
|
|
62
|
-
"simulo-interfaces>=0.
|
|
60
|
+
"simulo-interfaces>=0.26.0,<0.27",
|
|
63
61
|
"mcap>=1.3,<2",
|
|
64
62
|
# Hardened XML parsing for URDF ingestion (USD Asset Catalogs, PR-12 fix
|
|
65
63
|
# loop 2, security NIT): stdlib xml.etree.ElementTree relies on
|
|
@@ -70,6 +68,8 @@ dependencies = [
|
|
|
70
68
|
# featherweight promise below (that promise was about avoiding
|
|
71
69
|
# usd-core's tens of MB via an opt-in extra; usd-core itself joined the
|
|
72
70
|
# base footprint in 0.13.1 — see its own comment just below).
|
|
71
|
+
# Uncapped on purpose: the floor keeps the hardened XML parser; an upper
|
|
72
|
+
# bound is added only when a release breaks it.
|
|
73
73
|
"defusedxml>=0.7.1",
|
|
74
74
|
# usd-core: simulator-free but tens of MB — moved from the `assets`
|
|
75
75
|
# extra into the base install in 0.13.1 so `pip install simulo` covers
|
|
@@ -105,7 +105,7 @@ assets = ["usd-core>=26.5,<27"]
|
|
|
105
105
|
# `pytest-timeout` is pinned EXACT because it enforces the suite's hang bound;
|
|
106
106
|
# changing the plugin under an unchanged timeout would change that guard's behavior.
|
|
107
107
|
dev = [
|
|
108
|
-
"black==26.
|
|
108
|
+
"black==26.10.1",
|
|
109
109
|
"isort==8.0.1",
|
|
110
110
|
"ruff==0.15.21",
|
|
111
111
|
"mypy==2.3.0",
|
|
@@ -194,6 +194,7 @@ markers = [
|
|
|
194
194
|
"isaac: requires NVIDIA Isaac Sim / IsaacLab (skipped on dev machines and in PR CI)",
|
|
195
195
|
"gpu: requires a CUDA GPU (skipped on dev machines and in PR CI)",
|
|
196
196
|
"requires_execution_backend: end-to-end test that needs a live execution backend in the loop; skipped in CPU/unit runs",
|
|
197
|
+
"smoke: a short fundamental-path test; the hosted smoke job in simulo-ci.yml runs only these, and local CI runs every test",
|
|
197
198
|
]
|
|
198
199
|
|
|
199
200
|
# --- Formatters / linters --------------------------------------------------
|
|
@@ -316,8 +316,13 @@ def __getattr__(name: str) -> object:
|
|
|
316
316
|
Fires only for attributes missing from the module dict — i.e. never for the
|
|
317
317
|
real platform exports above. Delegates the learning names to the thin
|
|
318
318
|
client's internal resolver (discovery stand-in vs. real
|
|
319
|
-
``simulo.core`` class, by mode)
|
|
319
|
+
``simulo.core`` class, by mode), and read ``__version__`` from installed
|
|
320
|
+
package metadata on demand. Unknown names raise ``AttributeError``.
|
|
320
321
|
"""
|
|
322
|
+
if name == "__version__":
|
|
323
|
+
from importlib.metadata import version
|
|
324
|
+
|
|
325
|
+
return version("simulo")
|
|
321
326
|
if name in _LEARNING_NAMES:
|
|
322
327
|
return _resolve_learning(name)
|
|
323
328
|
removed_hints = {
|
|
@@ -23,7 +23,7 @@ from __future__ import annotations
|
|
|
23
23
|
|
|
24
24
|
import time
|
|
25
25
|
from pathlib import Path
|
|
26
|
-
from typing import Any, NoReturn, Optional
|
|
26
|
+
from typing import Any, Mapping, NoReturn, Optional
|
|
27
27
|
|
|
28
28
|
from simulo.interfaces.ids import JobId, JobPublicId
|
|
29
29
|
from simulo.interfaces.platform.enums import JobStatus, JobType
|
|
@@ -91,6 +91,8 @@ class JobHandle:
|
|
|
91
91
|
base_url: Optional[str] = None,
|
|
92
92
|
token: Optional[str] = None,
|
|
93
93
|
job_type: Optional[JobType] = None,
|
|
94
|
+
runtime_budget_s: Optional[int] = None,
|
|
95
|
+
job_args: Optional[Mapping[str, Any]] = None,
|
|
94
96
|
input_checkpoint: Optional[str] = None,
|
|
95
97
|
compare_checkpoint: Optional[str] = None,
|
|
96
98
|
deferred_stdout: str = "",
|
|
@@ -104,6 +106,8 @@ class JobHandle:
|
|
|
104
106
|
self._base_url = base_url
|
|
105
107
|
self._token = token
|
|
106
108
|
self._job_type = job_type
|
|
109
|
+
self._runtime_budget_s = runtime_budget_s
|
|
110
|
+
self._job_args = dict(job_args or {})
|
|
107
111
|
self._input_checkpoint = input_checkpoint
|
|
108
112
|
self._compare_checkpoint = compare_checkpoint
|
|
109
113
|
self._deferred_stdout = deferred_stdout
|
|
@@ -149,6 +153,16 @@ class JobHandle:
|
|
|
149
153
|
"""Declared type for a cloud submission; absent on legacy local handles."""
|
|
150
154
|
return self._job_type
|
|
151
155
|
|
|
156
|
+
@property
|
|
157
|
+
def runtime_budget_s(self) -> Optional[int]:
|
|
158
|
+
"""The effective server-resolved time limit, in seconds, when served."""
|
|
159
|
+
return self._runtime_budget_s
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def job_args(self) -> Mapping[str, Any]:
|
|
163
|
+
"""Training arguments for submit guidance, including declared defaults when known."""
|
|
164
|
+
return self._job_args
|
|
165
|
+
|
|
152
166
|
@property
|
|
153
167
|
def input_checkpoint(self) -> Optional[str]:
|
|
154
168
|
"""Policy checkpoint requested with ``simulo run --from``."""
|
|
@@ -50,12 +50,12 @@ from simulo._client import config, credentials, registry
|
|
|
50
50
|
from simulo._client._runner import JobFailedError, JobHandle, JobResultUnavailable
|
|
51
51
|
from simulo._client.repeatable_episodes import requested_repeatable_episodes
|
|
52
52
|
from simulo._client.runtime import DEFAULT_RUNTIME
|
|
53
|
+
from simulo._client.system_requests import effective_resources, validate_resources
|
|
53
54
|
from simulo._client.training_algorithm import validate_training_algorithm
|
|
54
55
|
from simulo.interfaces.ids import JobId, parse_job_public_id
|
|
55
56
|
from simulo.interfaces.platform import JobCallbackProtocol, JobType, normalize_job_tags
|
|
56
57
|
from simulo.interfaces.platform.app import Mount
|
|
57
58
|
from simulo.interfaces.platform.enums import SystemType
|
|
58
|
-
from simulo.interfaces.platform.systems import SYSTEM_SPECS
|
|
59
59
|
|
|
60
60
|
if TYPE_CHECKING: # typing-only; avoids a module-scope dependency on the HTTP client
|
|
61
61
|
from simulo._client.preflight_api import PreflightOutcome
|
|
@@ -564,6 +564,7 @@ def _spawn_cloud(
|
|
|
564
564
|
app_name: Optional[str] = None,
|
|
565
565
|
job_type: JobType,
|
|
566
566
|
tags: Sequence[str] = (),
|
|
567
|
+
job_args: Optional[Mapping[str, Any]] = None,
|
|
567
568
|
) -> JobHandle:
|
|
568
569
|
"""Tar the bundle, preflight it, upload it, create the job, and return a
|
|
569
570
|
cloud JobHandle.
|
|
@@ -627,7 +628,7 @@ def _spawn_cloud(
|
|
|
627
628
|
job-create call's top-level ``assets`` field, never ``args``.
|
|
628
629
|
"""
|
|
629
630
|
from simulo._client.asset_pins import (
|
|
630
|
-
|
|
631
|
+
asset_runtime_for_runtime_id,
|
|
631
632
|
collect_asset_refs,
|
|
632
633
|
resolve_and_pin_assets,
|
|
633
634
|
)
|
|
@@ -658,15 +659,12 @@ def _spawn_cloud(
|
|
|
658
659
|
package_id = str(package_id)
|
|
659
660
|
source_digest = str(manifest.get("source_digest", ""))
|
|
660
661
|
args = dict(manifest.get("args") or {})
|
|
661
|
-
#
|
|
662
|
-
#
|
|
663
|
-
#
|
|
664
|
-
# (the tier's wire value, e.g. "tier1"; see `decorate()`'s
|
|
665
|
-
# `resources["system"] = system.value`). Read from the already-parsed
|
|
666
|
-
# manifest rather than threaded in as a new parameter, since the value
|
|
667
|
-
# already lives there, keyed by this same `job_name`.
|
|
662
|
+
# CLI selections override this job's declared resources for this submit.
|
|
663
|
+
# They never enter args or rewrite the packaged manifest. Omitted options
|
|
664
|
+
# retain their existing wire representation for servers awaiting adoption.
|
|
668
665
|
job_entry = manifest.get("jobs", {}).get(job_name) or {}
|
|
669
|
-
|
|
666
|
+
allocation = effective_resources(job_entry.get("resources") or {})
|
|
667
|
+
system = allocation.pop("system", None)
|
|
670
668
|
manifest_version = manifest.get("manifest_version")
|
|
671
669
|
if not isinstance(manifest_version, int) or isinstance(manifest_version, bool):
|
|
672
670
|
raise RuntimeError(
|
|
@@ -719,6 +717,8 @@ def _spawn_cloud(
|
|
|
719
717
|
input_checkpoint=input_checkpoint,
|
|
720
718
|
viewstream=viewstream is True,
|
|
721
719
|
assets=[] if not asset_refs else None,
|
|
720
|
+
**({"system": system} if system is not None else {}),
|
|
721
|
+
**allocation,
|
|
722
722
|
)
|
|
723
723
|
deferred_stdout, deferred_stderr = _handle_preflight_outcome(outcome, job_name=job_name)
|
|
724
724
|
|
|
@@ -728,13 +728,15 @@ def _spawn_cloud(
|
|
|
728
728
|
# package is uploaded — so a missing/unpinned/unvalidated ref fails in seconds.
|
|
729
729
|
# Returns the top-level `assets: [{ref, digest}]` for create_job; `[]`
|
|
730
730
|
# when the app mounts no registry assets (no resolve call is made then).
|
|
731
|
-
asset_pins =
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
731
|
+
asset_pins: list[dict[str, str]] = []
|
|
732
|
+
if asset_refs:
|
|
733
|
+
asset_pins = resolve_and_pin_assets(
|
|
734
|
+
client,
|
|
735
|
+
asset_refs,
|
|
736
|
+
runtime=asset_runtime_for_runtime_id(str(manifest["runtime_id"])),
|
|
737
|
+
frozen=frozen_assets,
|
|
738
|
+
strict=strict_assets,
|
|
739
|
+
)
|
|
738
740
|
|
|
739
741
|
client.create_package(
|
|
740
742
|
package_id=package_id,
|
|
@@ -748,6 +750,7 @@ def _spawn_cloud(
|
|
|
748
750
|
package_id=package_id,
|
|
749
751
|
job_name=job_name,
|
|
750
752
|
args=args,
|
|
753
|
+
runtime_id=str(manifest["runtime_id"]),
|
|
751
754
|
viewstream=viewstream,
|
|
752
755
|
recording_copies=recording_copies,
|
|
753
756
|
repeatable_episodes=repeatable_episodes,
|
|
@@ -761,6 +764,7 @@ def _spawn_cloud(
|
|
|
761
764
|
job_type=job_type,
|
|
762
765
|
tags=tags,
|
|
763
766
|
manifest_version=manifest_version,
|
|
767
|
+
**allocation,
|
|
764
768
|
)
|
|
765
769
|
# A submitted entrypoint can call ``spawn()`` more than once. The
|
|
766
770
|
# resolved notice describes the run-level opt-in, so print it once only
|
|
@@ -794,6 +798,8 @@ def _spawn_cloud(
|
|
|
794
798
|
base_url=base_url,
|
|
795
799
|
token=token,
|
|
796
800
|
job_type=job_type,
|
|
801
|
+
runtime_budget_s=record.get("runtime_budget_s"),
|
|
802
|
+
job_args=job_args if job_args is not None else args,
|
|
797
803
|
input_checkpoint=input_checkpoint,
|
|
798
804
|
compare_checkpoint=compare_checkpoint,
|
|
799
805
|
deferred_stdout=deferred_stdout,
|
|
@@ -916,6 +922,12 @@ class JobFunction:
|
|
|
916
922
|
app_name=self._app.name,
|
|
917
923
|
job_type=self._spec.job_type,
|
|
918
924
|
tags=_JOB_TAGS_REQUESTED,
|
|
925
|
+
job_args={
|
|
926
|
+
name: parameter.default
|
|
927
|
+
for name, parameter in inspect.signature(self._spec.fn).parameters.items()
|
|
928
|
+
if parameter.default is not inspect.Parameter.empty
|
|
929
|
+
}
|
|
930
|
+
| kwargs,
|
|
919
931
|
)
|
|
920
932
|
|
|
921
933
|
if _SPAWN_RECORDER is None:
|
|
@@ -1302,6 +1314,8 @@ class App:
|
|
|
1302
1314
|
type: Union[JobType, str],
|
|
1303
1315
|
callbacks: Sequence[JobCallbackProtocol] = ...,
|
|
1304
1316
|
system: Optional[SystemType] = ...,
|
|
1317
|
+
gpus: Optional[int] = ...,
|
|
1318
|
+
gpus_per_node: Optional[int] = ...,
|
|
1305
1319
|
timeout: Optional[int] = ...,
|
|
1306
1320
|
retries: int = ...,
|
|
1307
1321
|
**extra: Any,
|
|
@@ -1315,6 +1329,8 @@ class App:
|
|
|
1315
1329
|
type: Union[JobType, str],
|
|
1316
1330
|
callbacks: Sequence[JobCallbackProtocol] = ...,
|
|
1317
1331
|
system: Optional[SystemType] = ...,
|
|
1332
|
+
gpus: Optional[int] = ...,
|
|
1333
|
+
gpus_per_node: Optional[int] = ...,
|
|
1318
1334
|
timeout: Optional[int] = ...,
|
|
1319
1335
|
retries: int = ...,
|
|
1320
1336
|
**extra: Any,
|
|
@@ -1327,6 +1343,8 @@ class App:
|
|
|
1327
1343
|
type: Optional[Union[JobType, str]] = None,
|
|
1328
1344
|
callbacks: Sequence[JobCallbackProtocol] = (),
|
|
1329
1345
|
system: Optional[SystemType] = None,
|
|
1346
|
+
gpus: Optional[int] = None,
|
|
1347
|
+
gpus_per_node: Optional[int] = None,
|
|
1330
1348
|
timeout: Optional[int] = None,
|
|
1331
1349
|
retries: int = 0,
|
|
1332
1350
|
**extra: Any,
|
|
@@ -1344,23 +1362,16 @@ class App:
|
|
|
1344
1362
|
pass it alongside keyword options).
|
|
1345
1363
|
callbacks: Lifecycle callbacks from :mod:`simulo.callbacks`, recorded
|
|
1346
1364
|
into the package manifest and honored at execution.
|
|
1347
|
-
system:
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
job AND scheduled against: the control plane filters a tiered
|
|
1358
|
-
worker's claim to its own tier plus untiered work, so the tier
|
|
1359
|
-
decides which GPU model runs the job. It is not a priority
|
|
1360
|
-
setting. ``simulo systems`` reports this package's catalog,
|
|
1361
|
-
not which tiers your platform has deployed. Naming a tier it
|
|
1362
|
-
has no fleet for is refused when you submit, and the error
|
|
1363
|
-
names the tiers it does have.
|
|
1365
|
+
system: Accelerator memory class as a :class:`simulo.SystemType` member.
|
|
1366
|
+
Omitted means Tier 1 (16 GB). Tier 2 is 24 GB, Tier 3 is 48 GB,
|
|
1367
|
+
and Tier 4 is 96 GB per accelerator. Hardware within a class can
|
|
1368
|
+
change. The built-in catalog enables Tier 1 and Tier 2 only;
|
|
1369
|
+
the server must also check its fleet readiness at submission.
|
|
1370
|
+
gpus: Total GPU count, a positive integer. Omitted means one GPU.
|
|
1371
|
+
Today's built-in policy permits one GPU per job.
|
|
1372
|
+
gpus_per_node: Optional positive integer layout constraint. All GPUs
|
|
1373
|
+
must run on one machine, so this must equal the total GPU count.
|
|
1374
|
+
It never multiplies ``gpus``. Counts reject booleans.
|
|
1364
1375
|
timeout: Requested runtime limit for one managed execution, in
|
|
1365
1376
|
seconds (a positive integer; validated HERE, at decoration
|
|
1366
1377
|
time — a zero, negative, or non-integer value raises
|
|
@@ -1385,7 +1396,7 @@ class App:
|
|
|
1385
1396
|
ValueError: ``timeout`` is not ``None`` and is not a positive
|
|
1386
1397
|
integer. ``True`` and ``False`` are not accepted as timeouts.
|
|
1387
1398
|
Also raised when ``system`` names a tier the platform cannot
|
|
1388
|
-
provision
|
|
1399
|
+
provision, or a GPU count/layout is unsupported by the built-in policy.
|
|
1389
1400
|
JobDeclarationError: ``produces=`` or ``consumes=`` was passed. Outputs are
|
|
1390
1401
|
not declared on the decorator. Callbacks declare their own
|
|
1391
1402
|
supported outputs. Return small JSON values from the job, or use a named
|
|
@@ -1427,28 +1438,12 @@ class App:
|
|
|
1427
1438
|
"@app.job(system=simulo.SystemType.TIER_1). Run `simulo systems` to list the "
|
|
1428
1439
|
"tiers and which are available today. Remove gpu=."
|
|
1429
1440
|
)
|
|
1430
|
-
if system is not None:
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
"@app.job(system=...) must be a simulo.SystemType member, e.g. "
|
|
1437
|
-
f"system=simulo.SystemType.TIER_1; got {system!r}. Run `simulo systems` to "
|
|
1438
|
-
"list the tiers."
|
|
1439
|
-
)
|
|
1440
|
-
spec = SYSTEM_SPECS[system]
|
|
1441
|
-
if not spec.available:
|
|
1442
|
-
available = ", ".join(
|
|
1443
|
-
f"SystemType.{tier.name} ({entry.gpu_model})"
|
|
1444
|
-
for tier, entry in SYSTEM_SPECS.items()
|
|
1445
|
-
if entry.available
|
|
1446
|
-
)
|
|
1447
|
-
raise ValueError(
|
|
1448
|
-
f"@app.job(system=SystemType.{system.name}) ({spec.gpu_model}) is not available on "
|
|
1449
|
-
f"the platform yet. Available today: {available}. Run `simulo systems` for the "
|
|
1450
|
-
"full catalog."
|
|
1451
|
-
)
|
|
1441
|
+
if system is not None and not isinstance(system, SystemType):
|
|
1442
|
+
raise JobDeclarationError(
|
|
1443
|
+
"@app.job(system=...) must be a simulo.SystemType member, e.g. "
|
|
1444
|
+
f"system=simulo.SystemType.TIER_1; got {system!r}. Run `simulo systems` to list the tiers."
|
|
1445
|
+
)
|
|
1446
|
+
requested_resources = validate_resources(system, gpus, gpus_per_node)
|
|
1452
1447
|
|
|
1453
1448
|
def decorate(func: F) -> JobFunction:
|
|
1454
1449
|
location = Path(func.__code__.co_filename).name
|
|
@@ -1466,9 +1461,7 @@ class App:
|
|
|
1466
1461
|
f"{location}: Job {func.__name__!r} has invalid type={type!r}. Valid job types: {valid}. "
|
|
1467
1462
|
'Example: @app.job(type="train").'
|
|
1468
1463
|
) from None
|
|
1469
|
-
resources
|
|
1470
|
-
if system is not None:
|
|
1471
|
-
resources["system"] = system.value
|
|
1464
|
+
resources = dict(requested_resources)
|
|
1472
1465
|
resources.update(extra)
|
|
1473
1466
|
policy_params = [
|
|
1474
1467
|
name
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""``Asset`` — a read-only, registry-resolved mount (implements ``AssetProtocol``).
|
|
2
2
|
|
|
3
3
|
A registry asset ref is ``[<publisher>/]<kind>/<name>[:v<N>]`` (e.g.
|
|
4
|
-
``robot/so-arm-100:v3``, ``simulo/world/warehouse:
|
|
4
|
+
``robot/so-arm-100:v3``, ``simulo/world/warehouse:v3``, or the unpinned
|
|
5
5
|
``robot/forklift`` — resolved to an exact version **at submit**). The grammar
|
|
6
6
|
is the ONE shared parser from ``simulo.interfaces.platform.asset_catalog`` — the
|
|
7
7
|
CLI, the control plane, the worker, and the console all parse through it, so the
|
|
@@ -148,7 +148,7 @@ class Asset:
|
|
|
148
148
|
"""A read-only, registry-resolved resource mounted into running jobs.
|
|
149
149
|
|
|
150
150
|
A registry ref is ``[<publisher>/]<kind>/<name>[:v<N>]`` (e.g.
|
|
151
|
-
``simulo/robot/cartpole:
|
|
151
|
+
``simulo/robot/cartpole:v5``); :meth:`from_registry` names one, and :attr:`path`
|
|
152
152
|
resolves to its read-only mount inside a running job. ``Asset`` is also how a
|
|
153
153
|
task names USD/URDF model sources to load into a scene — :meth:`usd` /
|
|
154
154
|
:meth:`urdf`.
|
|
@@ -24,14 +24,33 @@ from typing import Any, Dict, List, Mapping, Optional, Sequence, TextIO
|
|
|
24
24
|
|
|
25
25
|
from simulo._client.runtime_display import display_runtime
|
|
26
26
|
from simulo._client.submit_api import AssetPinError, SubmitApiClient
|
|
27
|
-
from simulo.interfaces.platform.asset_catalog import
|
|
27
|
+
from simulo.interfaces.platform.asset_catalog import (
|
|
28
|
+
DEFAULT_ASSET_RUNTIME,
|
|
29
|
+
ResolvedAssetPin,
|
|
30
|
+
parse_asset_ref,
|
|
31
|
+
)
|
|
32
|
+
from simulo.interfaces.platform.runtimes import RUNTIME_ID_BY_ASSET_RUNTIME
|
|
28
33
|
|
|
29
34
|
#: Re-exported from ``simulo.interfaces.platform.asset_catalog`` (current code and tests
|
|
30
35
|
#: "Runtime-string seam", 2026-07-17 — hoisted there so the CLIENT's resolve
|
|
31
36
|
#: request and the VALIDATOR's finalize call can never send different runtime
|
|
32
37
|
#: strings). Kept as a module attribute here too — existing call sites
|
|
33
38
|
#: (``app.py``, tests) import it from this module.
|
|
34
|
-
__all__ = ["DEFAULT_ASSET_RUNTIME", "collect_asset_refs", "resolve_and_pin_assets"]
|
|
39
|
+
__all__ = ["DEFAULT_ASSET_RUNTIME", "asset_runtime_for_runtime_id", "collect_asset_refs", "resolve_and_pin_assets"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def asset_runtime_for_runtime_id(runtime_id: str) -> str:
|
|
43
|
+
"""Return the catalog label published for a requested job runtime.
|
|
44
|
+
|
|
45
|
+
Asset validation records catalog labels while manifests request job runtime
|
|
46
|
+
identifiers. There is no safe label for an unlisted runtime: resolving an
|
|
47
|
+
asset under Isaac would make the validation result describe a different
|
|
48
|
+
runtime from the job that will mount it.
|
|
49
|
+
"""
|
|
50
|
+
for asset_runtime, mapped_runtime_id in RUNTIME_ID_BY_ASSET_RUNTIME.items():
|
|
51
|
+
if runtime_id == mapped_runtime_id:
|
|
52
|
+
return asset_runtime
|
|
53
|
+
raise AssetPinError(f"runtime {runtime_id!r} has no published asset catalog mapping")
|
|
35
54
|
|
|
36
55
|
|
|
37
56
|
def collect_asset_refs(manifest: Mapping[str, Any]) -> List[str]:
|
|
@@ -41,14 +41,12 @@ wheel's own bundled resources.
|
|
|
41
41
|
saved sign-in state; a failed clone is removed before it can appear at that path.
|
|
42
42
|
|
|
43
43
|
``simulo systems [--json]`` lists the published GPU system tiers (``SystemType`` /
|
|
44
|
-
``
|
|
44
|
+
``SYSTEM_TIERS`` from ``simulo-interfaces``) that ``@app.job(system=...)`` accepts —
|
|
45
45
|
table or ``--json``. This reads only that static, in-package catalog: no client
|
|
46
|
-
construction, no credential resolution, no network call.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
tiered worker to its own tier plus untiered work — so this listing is not a
|
|
51
|
-
record-only surface.
|
|
46
|
+
construction, no credential resolution, no network call. It shows each tier's memory
|
|
47
|
+
class, enabled GPU models, and static planning estimates for enabled tiers. It is not a
|
|
48
|
+
live fleet or price check; the server can apply additional deployment checks when a job
|
|
49
|
+
is submitted.
|
|
52
50
|
|
|
53
51
|
**Auth** (``simulo login`` / ``logout`` / ``whoami``) drives the OAuth 2.0 +
|
|
54
52
|
PKCE loopback flow and manages
|
|
@@ -108,6 +106,8 @@ import os
|
|
|
108
106
|
import sys
|
|
109
107
|
from typing import Optional, Sequence
|
|
110
108
|
|
|
109
|
+
from simulo._client import http
|
|
110
|
+
|
|
111
111
|
|
|
112
112
|
def _resolve_version() -> str:
|
|
113
113
|
"""Resolve the installed ``simulo`` distribution version for ``--version``.
|
|
@@ -209,6 +209,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
209
209
|
# interpreter shutdown, where a closed reader fails outside any handler.
|
|
210
210
|
sys.stdout.flush()
|
|
211
211
|
sys.stderr.flush()
|
|
212
|
+
except (http.InsecureTransportError, http.InsecureBearerError) as exc:
|
|
213
|
+
print(f"error: {_safe_cli_error_text(exc)}", file=sys.stderr)
|
|
214
|
+
return 1
|
|
212
215
|
except KeyboardInterrupt:
|
|
213
216
|
# Last-resort safety net: every dispatched command already has its own
|
|
214
217
|
# clean handling (``_observe_handler`` for the observe verbs,
|
|
@@ -170,6 +170,16 @@ def _relative_time(value: Any) -> str:
|
|
|
170
170
|
return f"on {_short_date(value)}"
|
|
171
171
|
|
|
172
172
|
|
|
173
|
+
def _runtime_limit_text(runtime_budget_s: int) -> str:
|
|
174
|
+
"""Render an exact server-resolved runtime budget in a compact unit."""
|
|
175
|
+
if runtime_budget_s % 60 == 0:
|
|
176
|
+
quantity = runtime_budget_s // 60
|
|
177
|
+
unit = "minute" if quantity == 1 else "minutes"
|
|
178
|
+
return f"{quantity} {unit}"
|
|
179
|
+
unit = "second" if runtime_budget_s == 1 else "seconds"
|
|
180
|
+
return f"{runtime_budget_s} {unit}"
|
|
181
|
+
|
|
182
|
+
|
|
173
183
|
def _print_table(headers: tuple[str, ...], rows: Sequence[tuple[str, ...]]) -> None:
|
|
174
184
|
"""The compact whitespace-aligned table used by CLI list commands
|
|
175
185
|
already render — factored out here as a shared helper for the newer
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Best-effort runtime guidance based on a saved source job's measured progress."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import shlex
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from simulo._client import http, preview_report
|
|
12
|
+
from simulo._client._runner import JobHandle
|
|
13
|
+
from simulo._client.cli_rendering import _runtime_limit_text
|
|
14
|
+
from simulo._client.jobs_api import JobsApiClient
|
|
15
|
+
from simulo._client.policies_api import PoliciesApiClient
|
|
16
|
+
from simulo.interfaces.ids import parse_checkpoint_ref, policy_id_for_job
|
|
17
|
+
from simulo.interfaces.platform.runs import JOB_SCOPE_ORG
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class CheckpointProgress:
|
|
22
|
+
iteration: int
|
|
23
|
+
iterations_per_minute: Optional[float] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _checkpoint(client: PoliciesApiClient, ref: str) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
27
|
+
policy_id, kind = parse_checkpoint_ref(ref)
|
|
28
|
+
policy = client.get_policy(str(policy_id))
|
|
29
|
+
checkpoints = policy.get("checkpoints")
|
|
30
|
+
if isinstance(checkpoints, list):
|
|
31
|
+
for checkpoint in checkpoints:
|
|
32
|
+
if isinstance(checkpoint, dict) and checkpoint.get("kind") == kind:
|
|
33
|
+
return policy, checkpoint
|
|
34
|
+
return policy, {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def checkpoint_progress(handle: JobHandle, ref: str, *, measure_rate: bool = False) -> Optional[CheckpointProgress]:
|
|
38
|
+
"""Read the selected checkpoint once; unavailable measurements leave submission intact."""
|
|
39
|
+
assert handle.base_url is not None
|
|
40
|
+
client = PoliciesApiClient(handle.base_url, token=handle.token)
|
|
41
|
+
progress: Optional[CheckpointProgress] = None
|
|
42
|
+
try:
|
|
43
|
+
policy, checkpoint = _checkpoint(client, ref)
|
|
44
|
+
iteration = checkpoint.get("iteration")
|
|
45
|
+
if type(iteration) is not int or iteration < 0:
|
|
46
|
+
return None
|
|
47
|
+
progress = CheckpointProgress(iteration)
|
|
48
|
+
if not measure_rate or not isinstance(policy.get("job_public_id"), str):
|
|
49
|
+
return progress
|
|
50
|
+
source = JobsApiClient(handle.base_url, token=handle.token).get_job(
|
|
51
|
+
policy["job_public_id"], scope=JOB_SCOPE_ORG
|
|
52
|
+
)
|
|
53
|
+
return CheckpointProgress(iteration, _source_rate(client, source, checkpoint))
|
|
54
|
+
except (ValueError, http.HttpError):
|
|
55
|
+
return progress
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _timestamp(value: Any) -> Optional[datetime]:
|
|
59
|
+
if not isinstance(value, str):
|
|
60
|
+
return None
|
|
61
|
+
try:
|
|
62
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
63
|
+
except ValueError:
|
|
64
|
+
return None
|
|
65
|
+
return parsed if parsed.tzinfo is not None else None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _source_rate(client: PoliciesApiClient, source: dict[str, Any], checkpoint: dict[str, Any]) -> Optional[float]:
|
|
69
|
+
"""Retries and unresolved starting checkpoints cannot give a reliable single-job rate."""
|
|
70
|
+
if source.get("attempts", 1) != 1:
|
|
71
|
+
return None
|
|
72
|
+
started, saved = _timestamp(source.get("started_at")), _timestamp(checkpoint.get("updated_at"))
|
|
73
|
+
if started is None or saved is None or saved <= started:
|
|
74
|
+
return None
|
|
75
|
+
start_iteration = 0
|
|
76
|
+
if source.get("input_checkpoint"):
|
|
77
|
+
_, starting_checkpoint = _checkpoint(client, source["input_checkpoint"])
|
|
78
|
+
candidate = starting_checkpoint.get("iteration")
|
|
79
|
+
if type(candidate) is not int or candidate < 0:
|
|
80
|
+
return None
|
|
81
|
+
start_iteration = candidate
|
|
82
|
+
iterations = checkpoint["iteration"] - start_iteration
|
|
83
|
+
return iterations * 60 / (saved - started).total_seconds() if iterations > 0 else None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def runtime_limit_lines(handle: JobHandle, progress: Optional[CheckpointProgress]) -> list[str]:
|
|
87
|
+
"""Estimate remaining iterations only when both a target and measured rate are known."""
|
|
88
|
+
budget = handle.runtime_budget_s
|
|
89
|
+
if budget is None:
|
|
90
|
+
return []
|
|
91
|
+
lines = [
|
|
92
|
+
f"Runtime limit: {_runtime_limit_text(budget)}. "
|
|
93
|
+
"The platform uses the lower of your organization's cap and any @app.job(timeout=...) limit. "
|
|
94
|
+
f"To raise your organization's cap, email {preview_report.SUPPORT_EMAIL}."
|
|
95
|
+
]
|
|
96
|
+
total = handle.job_args.get("max_iterations")
|
|
97
|
+
if (
|
|
98
|
+
progress is None
|
|
99
|
+
or progress.iterations_per_minute is None
|
|
100
|
+
or type(total) is not int
|
|
101
|
+
or total <= progress.iteration
|
|
102
|
+
):
|
|
103
|
+
return lines
|
|
104
|
+
minutes = (total - progress.iteration) / progress.iterations_per_minute
|
|
105
|
+
lines.append(
|
|
106
|
+
f"Estimated remaining training time: {_runtime_limit_text(math.ceil(minutes) * 60)} "
|
|
107
|
+
f"at the source job's rate of {progress.iterations_per_minute:.3g} iterations/min. "
|
|
108
|
+
"Measured from source job start to checkpoint save; startup time can differ."
|
|
109
|
+
)
|
|
110
|
+
if minutes * 60 > budget:
|
|
111
|
+
lines.append("Warning: the requested iterations are unlikely to finish within the runtime limit.")
|
|
112
|
+
return lines
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def runtime_recovery_lines(client: JobsApiClient, handle: JobHandle, app_path: str) -> list[str]:
|
|
116
|
+
"""A failed follow still reports its outcome if this best-effort read fails."""
|
|
117
|
+
job_ref = str(handle.public_id or handle.job_id)
|
|
118
|
+
try:
|
|
119
|
+
record = client.get_job(job_ref)
|
|
120
|
+
except http.HttpError:
|
|
121
|
+
return []
|
|
122
|
+
if record.get("status") != "timed_out" or record.get("status_reason") != "org_runtime_limit_exceeded":
|
|
123
|
+
return []
|
|
124
|
+
checkpoint = f"{policy_id_for_job(job_ref)}:latest"
|
|
125
|
+
return [
|
|
126
|
+
"Runtime limit reached.",
|
|
127
|
+
f"Check for a saved checkpoint: simulo policy get {checkpoint}",
|
|
128
|
+
f"To continue, use simulo run {shlex.quote(app_path)} --from {checkpoint} "
|
|
129
|
+
"with the original training arguments and a higher --max-iterations total.",
|
|
130
|
+
]
|