lagora-cli 1.1.0
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.
- package/README.md +138 -0
- package/dist/help.txt +70 -0
- package/dist/lagora.js +342 -0
- package/dist/report-help.txt +5 -0
- package/dist/scripts/agora_playground_harness.py +263 -0
- package/dist/scripts/announce.js +41 -0
- package/dist/scripts/check-kernel-submission.py +90 -0
- package/dist/scripts/chunk-2EAJVB5D.js +100 -0
- package/dist/scripts/chunk-2KTLCUFI.js +29 -0
- package/dist/scripts/chunk-AZ3EEBVD.js +137 -0
- package/dist/scripts/chunk-NBJMYAOA.js +2128 -0
- package/dist/scripts/chunk-NCJMUBTG.js +125 -0
- package/dist/scripts/chunk-QJPQHKIO.js +23 -0
- package/dist/scripts/chunk-RIR5KGHC.js +33 -0
- package/dist/scripts/chunk-TJZVQYBL.js +8 -0
- package/dist/scripts/chunk-UHJXD4TG.js +18 -0
- package/dist/scripts/chunk-UQ6I6VTY.js +117 -0
- package/dist/scripts/cli-auth.js +348 -0
- package/dist/scripts/cli-config-IA7EOSYD.js +7 -0
- package/dist/scripts/install-skill.js +199 -0
- package/dist/scripts/issue-local-client-DZUXZOKY.js +22 -0
- package/dist/scripts/issue-search.js +1823 -0
- package/dist/scripts/issue.js +386 -0
- package/dist/scripts/keycloak-provision.js +986 -0
- package/dist/scripts/legato-fsim-runner.py +126 -0
- package/dist/scripts/legato-lowering-runner.py +156 -0
- package/dist/scripts/legato_runner_annotations.py +235 -0
- package/dist/scripts/legato_runner_env.py +91 -0
- package/dist/scripts/legato_runner_launchers.py +287 -0
- package/dist/scripts/legato_runner_script_wrapper.py +193 -0
- package/dist/scripts/notifications-EU43SIEV.js +624 -0
- package/dist/scripts/playground.js +408 -0
- package/dist/scripts/report-bundle-sync-3U7QTP4Z.js +215 -0
- package/dist/scripts/report.js +104 -0
- package/dist/scripts/resolve-sdk-package-version.py +151 -0
- package/dist/scripts/sdk-runtime-JE6H2PB2.js +992 -0
- package/dist/scripts/sdk-runtime-kubernetes-job-KOWL4ITV.js +479 -0
- package/dist/scripts/sdk-runtime-smoke.py +168 -0
- package/dist/scripts/sdk.js +256 -0
- package/dist/scripts/site-feedback-CAPE5MPX.js +136 -0
- package/dist/scripts/site-feedback-rate-limit-5BU2WSFE.js +86 -0
- package/dist/scripts/site-feedback.js +117 -0
- package/dist/scripts/storage-234FBH54.js +67 -0
- package/dist/scripts/submit-issue.sh +489 -0
- package/dist/scripts/verification-3QCY66QW.js +772 -0
- package/dist/scripts/verify-issue.js +144 -0
- package/dist/skills/legato-agora-cli/SKILL.md +556 -0
- package/dist/skills/legato-agora-cli/agents/openai.yaml +7 -0
- package/dist/skills/legato-agora-cli/reference/kernel-with-golden.py +84 -0
- package/dist/skills/legato-site-feedback/SKILL.md +49 -0
- package/dist/skills/legato-site-feedback/agents/openai.yaml +7 -0
- package/package.json +16 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
import traceback
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from agora_playground_harness import has_playground_contract, run_playground_kernel
|
|
10
|
+
from legato_runner_launchers import LauncherError, LauncherScriptError, launcher_runs_in_subprocess, run_launcher
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FsimPreflightError(RuntimeError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_args() -> argparse.Namespace:
|
|
18
|
+
parser = argparse.ArgumentParser(description="Run a Legato BINARY launcher on HART fsim")
|
|
19
|
+
parser.add_argument("--kernel", required=True, help="Path to uploaded Python launcher/kernel file")
|
|
20
|
+
parser.add_argument("--output-root", required=True, help="Directory for generated fsim outputs")
|
|
21
|
+
return parser.parse_args()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def export_fsim_child_config(output_root: Path) -> None:
|
|
25
|
+
"""Hand the fsim configuration to the process that actually runs the kernel.
|
|
26
|
+
|
|
27
|
+
Script-style launchers execute in a child process, and hart's device state
|
|
28
|
+
lives in the C++ runtime of whichever process called set_fsim_env. Opening
|
|
29
|
+
the device here would leave the child without one.
|
|
30
|
+
"""
|
|
31
|
+
os.environ.setdefault("HA_TOOLCHAIN", "1")
|
|
32
|
+
os.environ.setdefault("HA_MOCK_DEVICE_COUNT", "2")
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
import hart # noqa: F401 fail fast if the SDK cannot provide fsim at all
|
|
36
|
+
except ImportError as exc:
|
|
37
|
+
raise FsimPreflightError(f"hart not available: {exc}") from exc
|
|
38
|
+
|
|
39
|
+
os.environ["AGORA_FSIM_DEVICE_VERSION"] = os.environ.get("AGORA_FSIM_DEVICE_VERSION", "BERTHA_CLOUD_EVT0")
|
|
40
|
+
os.environ["AGORA_FSIM_COMPUTE_MODE"] = os.environ.get("AGORA_FSIM_COMPUTE_MODE", "TORCH")
|
|
41
|
+
os.environ["AGORA_FSIM_LOG_LEVEL"] = os.environ.get("AGORA_FSIM_LOG_LEVEL", "trace")
|
|
42
|
+
os.environ["AGORA_FSIM_LOG_PATH"] = str(output_root / "fsim.log")
|
|
43
|
+
os.environ["AGORA_FSIM_CONFIGURE_IN_CHILD"] = "1"
|
|
44
|
+
print("[LEGATO_FSIM] configure=child", flush=True)
|
|
45
|
+
print(f"[LEGATO_FSIM] device_version={os.environ['AGORA_FSIM_DEVICE_VERSION']}", flush=True)
|
|
46
|
+
print(f"[LEGATO_FSIM] compute_mode={os.environ['AGORA_FSIM_COMPUTE_MODE']}", flush=True)
|
|
47
|
+
print(f"[LEGATO_FSIM] log_path={os.environ['AGORA_FSIM_LOG_PATH']}", flush=True)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def configure_fsim(output_root: Path) -> None:
|
|
51
|
+
os.environ.setdefault("HA_TOOLCHAIN", "1")
|
|
52
|
+
os.environ.setdefault("HA_MOCK_DEVICE_COUNT", "2")
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
import hart
|
|
56
|
+
except ImportError as exc:
|
|
57
|
+
raise FsimPreflightError(f"hart not available: {exc}") from exc
|
|
58
|
+
|
|
59
|
+
device_version_name = os.environ.get("AGORA_FSIM_DEVICE_VERSION", "BERTHA_CLOUD_EVT0")
|
|
60
|
+
compute_mode_name = os.environ.get("AGORA_FSIM_COMPUTE_MODE", "TORCH")
|
|
61
|
+
log_level = os.environ.get("AGORA_FSIM_LOG_LEVEL", "trace")
|
|
62
|
+
log_path = output_root / "fsim.log"
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
device_version = getattr(hart.HaDeviceVersion, device_version_name)
|
|
66
|
+
compute_mode = getattr(hart.HaFSimComputeMode, compute_mode_name)
|
|
67
|
+
except AttributeError as exc:
|
|
68
|
+
raise FsimPreflightError(
|
|
69
|
+
f"unsupported fsim config: AGORA_FSIM_DEVICE_VERSION={device_version_name}, "
|
|
70
|
+
f"AGORA_FSIM_COMPUTE_MODE={compute_mode_name}",
|
|
71
|
+
) from exc
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
hart.set_fsim_env(device_version, compute_mode, str(log_path), log_level)
|
|
75
|
+
except RuntimeError as exc:
|
|
76
|
+
raise FsimPreflightError(f"set_fsim_env failed: {exc}") from exc
|
|
77
|
+
if hart.get_device_count() == 0:
|
|
78
|
+
raise FsimPreflightError("no fsim devices available")
|
|
79
|
+
hart.set_device(0)
|
|
80
|
+
print(f"[LEGATO_FSIM] device_version={device_version_name}", flush=True)
|
|
81
|
+
print(f"[LEGATO_FSIM] compute_mode={compute_mode_name}", flush=True)
|
|
82
|
+
print(f"[LEGATO_FSIM] log_path={log_path}", flush=True)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main() -> int:
|
|
86
|
+
args = parse_args()
|
|
87
|
+
kernel_path = Path(args.kernel).resolve()
|
|
88
|
+
output_root = Path(args.output_root).resolve()
|
|
89
|
+
output_root.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
|
|
91
|
+
# A kernel that brings no launcher of its own is driven here rather than by
|
|
92
|
+
# itself, so the device has to be opened in this process -- not handed to a
|
|
93
|
+
# child that is never spawned.
|
|
94
|
+
harness_driven = has_playground_contract(kernel_path)
|
|
95
|
+
try:
|
|
96
|
+
if harness_driven or not launcher_runs_in_subprocess(kernel_path):
|
|
97
|
+
configure_fsim(output_root)
|
|
98
|
+
else:
|
|
99
|
+
export_fsim_child_config(output_root)
|
|
100
|
+
os.environ["AGORA_LEGATO_SESSION_LAUNCH"] = "1"
|
|
101
|
+
print("[LEGATO_WORKER] stage=FSIM status=running", flush=True)
|
|
102
|
+
if harness_driven:
|
|
103
|
+
print("[LEGATO_WORKER] launcher_mode=harness", flush=True)
|
|
104
|
+
run_playground_kernel(kernel_path, output_root)
|
|
105
|
+
else:
|
|
106
|
+
run_launcher(kernel_path, ["BINARY"], output_root)
|
|
107
|
+
except LauncherScriptError as exc:
|
|
108
|
+
print("[LEGATO_WORKER] stage=FSIM status=failed", flush=True)
|
|
109
|
+
print(f"[LEGATO_WORKER] reason={exc}", flush=True)
|
|
110
|
+
return 1
|
|
111
|
+
except (FsimPreflightError, LauncherError) as exc:
|
|
112
|
+
print("[LEGATO_WORKER] preflight=failed", flush=True)
|
|
113
|
+
print(f"[LEGATO_WORKER] reason={exc}", flush=True)
|
|
114
|
+
return 2
|
|
115
|
+
except Exception: # noqa: BROAD_EXCEPT_OK
|
|
116
|
+
print("[LEGATO_WORKER] stage=FSIM status=failed", flush=True)
|
|
117
|
+
traceback.print_exc()
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
print("[LEGATO_WORKER] stage=FSIM status=success", flush=True)
|
|
121
|
+
print("[LEGATO_WORKER] fsim=success", flush=True)
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run staged Legato lowering for an uploaded kernel module.
|
|
3
|
+
|
|
4
|
+
The runner imports a user kernel file, finds a @legato.compile or
|
|
5
|
+
@legato.primitive JitFunction, synthesizes dummy arguments from Legato type
|
|
6
|
+
annotations, and runs the same function through each requested OutputType. It
|
|
7
|
+
prints stable worker markers so the Agora TypeScript worker can summarize the
|
|
8
|
+
failed stage.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import ast
|
|
15
|
+
import importlib.util
|
|
16
|
+
import inspect
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
import traceback
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from legato_runner_annotations import (
|
|
25
|
+
PreflightError,
|
|
26
|
+
load_module,
|
|
27
|
+
place_arguments,
|
|
28
|
+
select_kernel,
|
|
29
|
+
synthesize_args,
|
|
30
|
+
top_level_function_names,
|
|
31
|
+
)
|
|
32
|
+
from legato_runner_launchers import (
|
|
33
|
+
LauncherError,
|
|
34
|
+
LauncherScriptError,
|
|
35
|
+
kernel_has_launcher,
|
|
36
|
+
run_launcher,
|
|
37
|
+
statically_decorated_kernel_names,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
DEFAULT_STAGES = ["MLIR", "CORE_IR", "BACKEND_IR", "ASM", "BINARY"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_args() -> argparse.Namespace:
|
|
45
|
+
parser = argparse.ArgumentParser(description="Run staged Legato lowering")
|
|
46
|
+
parser.add_argument("--kernel", required=True, help="Path to uploaded kernel.py")
|
|
47
|
+
parser.add_argument("--function", help="@legato.compile/@legato.primitive function name. Auto-detects if omitted.")
|
|
48
|
+
parser.add_argument("--output-root", required=True, help="Directory for stage outputs")
|
|
49
|
+
parser.add_argument("--stages", default=",".join(DEFAULT_STAGES), help="Comma-separated OutputType stages")
|
|
50
|
+
return parser.parse_args()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run_stage(legato_module: Any, kernel: Any, kernel_args: list[Any], stage: str, output_root: Path) -> None:
|
|
56
|
+
output_type = getattr(legato_module.OutputType, stage)
|
|
57
|
+
output_path = output_root / stage.lower()
|
|
58
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
print(f"[LEGATO_WORKER] stage={stage} status=running", flush=True)
|
|
60
|
+
print(f"[LEGATO_WORKER] output_path={output_path}", flush=True)
|
|
61
|
+
with legato_module.session(**session_kwargs(legato_module.session, output_type, output_path, launch=False)):
|
|
62
|
+
kernel(*kernel_args)
|
|
63
|
+
print(f"[LEGATO_WORKER] stage={stage} status=success", flush=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def session_kwargs(session_factory: Any, output_type: Any, output_path: Path, launch: bool) -> dict[str, Any]:
|
|
67
|
+
kwargs: dict[str, Any] = {"output_type": output_type, "output_path": str(output_path)}
|
|
68
|
+
if session_accepts_launch(session_factory):
|
|
69
|
+
kwargs["launch"] = launch
|
|
70
|
+
return kwargs
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def session_accepts_launch(session_factory: Any) -> bool:
|
|
74
|
+
try:
|
|
75
|
+
signature = inspect.signature(session_factory)
|
|
76
|
+
except (TypeError, ValueError):
|
|
77
|
+
return False
|
|
78
|
+
return "launch" in signature.parameters or any(
|
|
79
|
+
parameter.kind is inspect.Parameter.VAR_KEYWORD
|
|
80
|
+
for parameter in signature.parameters.values()
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def validate_stages(legato_module: Any, stages: list[str]) -> None:
|
|
85
|
+
if not stages:
|
|
86
|
+
raise PreflightError("no lowering stages requested")
|
|
87
|
+
for stage in stages:
|
|
88
|
+
if not hasattr(legato_module.OutputType, stage):
|
|
89
|
+
raise PreflightError(f"unsupported lowering stage: {stage}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def run_compiled_kernel(legato_module: Any, kernel_path: Path, function_name: str | None, stages: list[str], output_root: Path) -> None:
|
|
93
|
+
local_function_names = top_level_function_names(kernel_path)
|
|
94
|
+
selected_name: str | None = None
|
|
95
|
+
for index, stage in enumerate(stages):
|
|
96
|
+
module = load_module(kernel_path)
|
|
97
|
+
stage_selected_name, kernel = select_kernel(module, function_name, local_function_names)
|
|
98
|
+
if selected_name is None:
|
|
99
|
+
selected_name = stage_selected_name
|
|
100
|
+
print(f"[LEGATO_WORKER] kernel_function={selected_name}")
|
|
101
|
+
elif stage_selected_name != selected_name:
|
|
102
|
+
raise PreflightError(f"selected kernel changed between stages: {selected_name} != {stage_selected_name}")
|
|
103
|
+
# A kernel that declares param_kinds must be called with its arguments
|
|
104
|
+
# bound to those targets, or lowering refuses them as bare values.
|
|
105
|
+
kernel_args = place_arguments(kernel, synthesize_args(kernel))
|
|
106
|
+
if index == 0:
|
|
107
|
+
print(f"[LEGATO_WORKER] synthesized_args={len(kernel_args)}")
|
|
108
|
+
run_stage(legato_module, kernel, kernel_args, stage, output_root)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main() -> int:
|
|
112
|
+
args = parse_args()
|
|
113
|
+
kernel_path = Path(args.kernel).resolve()
|
|
114
|
+
output_root = Path(args.output_root).resolve()
|
|
115
|
+
stages = [stage.strip() for stage in args.stages.split(",") if stage.strip()]
|
|
116
|
+
function_name = args.function or os.environ.get("LEGATO_KERNEL_FUNCTION")
|
|
117
|
+
os.environ.setdefault("AGORA_LEGATO_SESSION_LAUNCH", "0")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
import legato
|
|
121
|
+
except Exception: # noqa: BLE001
|
|
122
|
+
print("[LEGATO_WORKER] preflight=failed")
|
|
123
|
+
print("[LEGATO_WORKER] reason=failed to import legato")
|
|
124
|
+
traceback.print_exc()
|
|
125
|
+
return 2
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
validate_stages(legato, stages)
|
|
129
|
+
decorated_names = statically_decorated_kernel_names(kernel_path)
|
|
130
|
+
if function_name:
|
|
131
|
+
run_compiled_kernel(legato, kernel_path, function_name, stages, output_root)
|
|
132
|
+
elif kernel_has_launcher(kernel_path):
|
|
133
|
+
run_launcher(kernel_path, stages, output_root)
|
|
134
|
+
elif decorated_names:
|
|
135
|
+
run_compiled_kernel(legato, kernel_path, None, stages, output_root)
|
|
136
|
+
else:
|
|
137
|
+
run_launcher(kernel_path, stages, output_root)
|
|
138
|
+
except LauncherScriptError as exc:
|
|
139
|
+
print("[LEGATO_WORKER] lowering_pipeline=failed")
|
|
140
|
+
print(f"[LEGATO_WORKER] reason={exc}")
|
|
141
|
+
return 1
|
|
142
|
+
except (PreflightError, LauncherError) as exc:
|
|
143
|
+
print("[LEGATO_WORKER] preflight=failed")
|
|
144
|
+
print(f"[LEGATO_WORKER] reason={exc}")
|
|
145
|
+
return 2
|
|
146
|
+
except Exception: # noqa: BLE001
|
|
147
|
+
print("[LEGATO_WORKER] lowering_pipeline=failed")
|
|
148
|
+
traceback.print_exc()
|
|
149
|
+
return 1
|
|
150
|
+
|
|
151
|
+
print("[LEGATO_WORKER] lowering_pipeline=success")
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Reading a Legato kernel's shape from its annotations.
|
|
2
|
+
|
|
3
|
+
Extracted so more than one runner can use it: lowering synthesizes arguments to
|
|
4
|
+
compile with, and the Playground harness needs the same shapes to allocate device
|
|
5
|
+
tensors. The file name matters -- the runners are hyphenated scripts and cannot
|
|
6
|
+
be imported, which is why this had to move before it could be shared.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import importlib.util
|
|
12
|
+
import inspect
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
TORCH_DTYPE_BY_LEGATO = {
|
|
19
|
+
"bfloat16": "bfloat16",
|
|
20
|
+
"bf16": "bfloat16",
|
|
21
|
+
"float16": "float16",
|
|
22
|
+
"fp16": "float16",
|
|
23
|
+
"float32": "float32",
|
|
24
|
+
"f32": "float32",
|
|
25
|
+
"float64": "float64",
|
|
26
|
+
"f64": "float64",
|
|
27
|
+
"int32": "int32",
|
|
28
|
+
"i32": "int32",
|
|
29
|
+
"int64": "int64",
|
|
30
|
+
"i64": "int64",
|
|
31
|
+
"bool": "bool",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PreflightError(RuntimeError):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_module(kernel_path: Path) -> Any:
|
|
40
|
+
spec = importlib.util.spec_from_file_location("agora_uploaded_kernel", kernel_path)
|
|
41
|
+
if spec is None or spec.loader is None:
|
|
42
|
+
raise PreflightError(f"cannot import kernel module: {kernel_path}")
|
|
43
|
+
module = importlib.util.module_from_spec(spec)
|
|
44
|
+
sys.path.insert(0, str(kernel_path.parent))
|
|
45
|
+
try:
|
|
46
|
+
spec.loader.exec_module(module)
|
|
47
|
+
finally:
|
|
48
|
+
if sys.path and sys.path[0] == str(kernel_path.parent):
|
|
49
|
+
sys.path.pop(0)
|
|
50
|
+
return module
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_legato_jit_function(value: Any) -> bool:
|
|
54
|
+
return callable(value) and hasattr(value, "_node") and hasattr(value, "_globals") and hasattr(value, "verify_types")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def top_level_function_names(kernel_path: Path) -> set[str]:
|
|
58
|
+
with kernel_path.open("r", encoding="utf-8") as source:
|
|
59
|
+
tree = ast.parse(source.read(), filename=str(kernel_path))
|
|
60
|
+
return {node.name for node in tree.body if isinstance(node, ast.FunctionDef)}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def select_kernel(module: Any, function_name: str | None, local_function_names: set[str]) -> tuple[str, Any]:
|
|
64
|
+
if function_name:
|
|
65
|
+
value = getattr(module, function_name, None)
|
|
66
|
+
if not is_legato_jit_function(value):
|
|
67
|
+
raise PreflightError(f"{function_name} is not a @legato.compile/@legato.primitive function")
|
|
68
|
+
return function_name, value
|
|
69
|
+
|
|
70
|
+
candidates = [
|
|
71
|
+
(name, value)
|
|
72
|
+
for name, value in vars(module).items()
|
|
73
|
+
if name in local_function_names and is_legato_jit_function(value)
|
|
74
|
+
]
|
|
75
|
+
if not candidates:
|
|
76
|
+
raise PreflightError("no @legato.compile/@legato.primitive function found in uploaded module")
|
|
77
|
+
if len(candidates) > 1:
|
|
78
|
+
names = ", ".join(name for name, _ in candidates)
|
|
79
|
+
raise PreflightError(f"multiple @legato.compile/@legato.primitive functions found; set LEGATO_KERNEL_FUNCTION. candidates={names}")
|
|
80
|
+
return candidates[0]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_function_def(jit_function: Any) -> ast.FunctionDef:
|
|
84
|
+
node = getattr(jit_function, "_node", None)
|
|
85
|
+
for item in ast.walk(node):
|
|
86
|
+
if isinstance(item, ast.FunctionDef):
|
|
87
|
+
return item
|
|
88
|
+
raise PreflightError("cannot locate function AST for Legato kernel target")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def annotation_source(annotation: ast.expr | None) -> str:
|
|
92
|
+
if annotation is None:
|
|
93
|
+
return ""
|
|
94
|
+
return ast.unparse(annotation)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def eval_annotation(source: str, jit_function: Any) -> Any:
|
|
98
|
+
if not source:
|
|
99
|
+
raise PreflightError("missing required Legato argument annotation")
|
|
100
|
+
namespace = dict(getattr(jit_function, "_globals", {}))
|
|
101
|
+
return eval(source, namespace) # noqa: S307 - trusted internal worker environment for uploaded repros.
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def dtype_name(type_value: Any, source: str) -> str:
|
|
105
|
+
quoted = re.search(r"['\"]([A-Za-z0-9_]+)['\"]", source)
|
|
106
|
+
if quoted and quoted.group(1).lower() in TORCH_DTYPE_BY_LEGATO:
|
|
107
|
+
return TORCH_DTYPE_BY_LEGATO[quoted.group(1).lower()]
|
|
108
|
+
text = str(type_value).lower()
|
|
109
|
+
for legato_name, torch_name in TORCH_DTYPE_BY_LEGATO.items():
|
|
110
|
+
if legato_name in text:
|
|
111
|
+
return torch_name
|
|
112
|
+
return "float32"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def dimension_value(dim: Any) -> int:
|
|
116
|
+
if isinstance(dim, int):
|
|
117
|
+
return 8 if dim == -1 else max(dim, 1)
|
|
118
|
+
if isinstance(dim, tuple):
|
|
119
|
+
if len(dim) == 2 and dim[0] == -1 and isinstance(dim[1], int):
|
|
120
|
+
return max(dim[1], 1)
|
|
121
|
+
raise PreflightError(f"unsupported dynamic dimension tuple: {dim!r}")
|
|
122
|
+
raise PreflightError(f"unsupported tensor dimension: {dim!r}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def tensor_shape_from_annotation_source(source: str, jit_function: Any) -> list[int]:
|
|
126
|
+
parsed = ast.parse(source, mode="eval")
|
|
127
|
+
call = parsed.body
|
|
128
|
+
if not isinstance(call, ast.Call):
|
|
129
|
+
raise PreflightError(f"unsupported tensor annotation expression: {source}")
|
|
130
|
+
if len(call.args) < 2:
|
|
131
|
+
raise PreflightError(f"tensor annotation is missing shape argument: {source}")
|
|
132
|
+
shape_node = call.args[1]
|
|
133
|
+
try:
|
|
134
|
+
raw_shape = ast.literal_eval(shape_node)
|
|
135
|
+
except Exception as exc: # noqa: BLE001
|
|
136
|
+
namespace = dict(getattr(jit_function, "_globals", {}))
|
|
137
|
+
try:
|
|
138
|
+
raw_shape = eval(ast.unparse(shape_node), namespace) # noqa: S307 - internal repro runner.
|
|
139
|
+
except Exception as eval_exc: # noqa: BLE001
|
|
140
|
+
raise PreflightError(f"tensor shape must be literal/evaluable: {source}") from eval_exc
|
|
141
|
+
if isinstance(raw_shape, int):
|
|
142
|
+
return [dimension_value(raw_shape)]
|
|
143
|
+
if not isinstance(raw_shape, (list, tuple)):
|
|
144
|
+
raise PreflightError(f"tensor shape must be a list/tuple literal: {source}")
|
|
145
|
+
return [dimension_value(dim) for dim in raw_shape]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def is_tensor_annotation(value: Any, source: str) -> bool:
|
|
149
|
+
if "tensor_type" in source or ".tensor(" in source:
|
|
150
|
+
return True
|
|
151
|
+
return hasattr(value, "is_tensor") and bool(value.is_tensor())
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def scalar_arg(type_value: Any) -> Any:
|
|
155
|
+
text = str(type_value).lower()
|
|
156
|
+
if "bool" in text:
|
|
157
|
+
return True
|
|
158
|
+
if "float" in text or "f32" in text or "f64" in text:
|
|
159
|
+
return 1.0
|
|
160
|
+
if "list" in text:
|
|
161
|
+
return [0]
|
|
162
|
+
if "tuple" in text:
|
|
163
|
+
return (0, 0)
|
|
164
|
+
return 1
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def make_tensor_arg(torch_module: Any, type_value: Any, source: str, name: str, jit_function: Any) -> Any:
|
|
168
|
+
shape = tensor_shape_from_annotation_source(source, jit_function)
|
|
169
|
+
dtype = getattr(torch_module, dtype_name(type_value, source))
|
|
170
|
+
if name.lower() in {"out", "output", "result"} or name.lower().endswith(("_out", "_output")):
|
|
171
|
+
return torch_module.zeros(*shape, dtype=dtype)
|
|
172
|
+
if dtype is torch_module.bool:
|
|
173
|
+
return torch_module.zeros(*shape, dtype=dtype)
|
|
174
|
+
if dtype in {torch_module.int32, torch_module.int64}:
|
|
175
|
+
return torch_module.ones(*shape, dtype=dtype)
|
|
176
|
+
return torch_module.randn(*shape, dtype=dtype)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def synthesize_args(jit_function: Any) -> list[Any]:
|
|
180
|
+
function_def = get_function_def(jit_function)
|
|
181
|
+
if not function_def.args.args:
|
|
182
|
+
return []
|
|
183
|
+
|
|
184
|
+
import torch
|
|
185
|
+
|
|
186
|
+
args: list[Any] = []
|
|
187
|
+
for arg in function_def.args.args:
|
|
188
|
+
source = annotation_source(arg.annotation)
|
|
189
|
+
type_value = eval_annotation(source, jit_function)
|
|
190
|
+
if is_tensor_annotation(type_value, source):
|
|
191
|
+
args.append(make_tensor_arg(torch, type_value, source, arg.arg, jit_function))
|
|
192
|
+
else:
|
|
193
|
+
args.append(scalar_arg(type_value))
|
|
194
|
+
return args
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def placement_for(jit_function: Any, name: str) -> Any:
|
|
198
|
+
"""Where a parameter has to be bound, from the decorator's param_kinds.
|
|
199
|
+
|
|
200
|
+
None means pass the tensor as it is; anything else has to be wrapped in
|
|
201
|
+
legato.Arg. A kernel that declares a placement and is called without one
|
|
202
|
+
fails at lowering with "declared as 'top_param' but passed as a bare value",
|
|
203
|
+
so both the lowering path and the Playground harness ask this.
|
|
204
|
+
"""
|
|
205
|
+
import legato.model.bertha as bertha
|
|
206
|
+
|
|
207
|
+
# The compiled function keeps these as _param_kinds; reading the public
|
|
208
|
+
# spelling silently found nothing and left every argument unbound.
|
|
209
|
+
kinds = getattr(jit_function, "_param_kinds", None)
|
|
210
|
+
if kinds is None:
|
|
211
|
+
kinds = getattr(jit_function, "param_kinds", None) or {}
|
|
212
|
+
# Stored normalised as "<kind>_param"; the decorator is written with the bare
|
|
213
|
+
# word. Accept either rather than depending on which spelling survives.
|
|
214
|
+
kind = kinds.get(name)
|
|
215
|
+
if isinstance(kind, str) and kind.endswith("_param"):
|
|
216
|
+
kind = kind[: -len("_param")]
|
|
217
|
+
if kind in (None, "universal"):
|
|
218
|
+
return None
|
|
219
|
+
if kind == "top":
|
|
220
|
+
return bertha.top
|
|
221
|
+
if kind == "core":
|
|
222
|
+
return bertha.core(0)
|
|
223
|
+
raise PreflightError(f"parameter '{name}' has param_kind '{kind}', which is not placed automatically")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def place_arguments(jit_function: Any, values: list[Any]) -> list[Any]:
|
|
227
|
+
"""Bind each argument to the device target its parameter declares."""
|
|
228
|
+
import legato
|
|
229
|
+
|
|
230
|
+
names = [arg.arg for arg in get_function_def(jit_function).args.args]
|
|
231
|
+
placed: list[Any] = []
|
|
232
|
+
for name, value in zip(names, values):
|
|
233
|
+
target = placement_for(jit_function, name)
|
|
234
|
+
placed.append(value if target is None else legato.Arg(value, target))
|
|
235
|
+
return placed
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def launcher_working_directory() -> Path:
|
|
9
|
+
return Path(os.environ.get("AGORA_SDK_ROOT", Path.cwd())).resolve()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def compiler_root() -> Path | None:
|
|
13
|
+
"""Root of a separate SDK supplying legato, when one was selected."""
|
|
14
|
+
configured = os.environ.get("AGORA_LEGATO_COMPILER_ROOT", "").strip()
|
|
15
|
+
return Path(configured).resolve() if configured else None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def legato_source_path(root: Path) -> Path | None:
|
|
19
|
+
"""The checkout's legato package, but only when it was built in place.
|
|
20
|
+
|
|
21
|
+
A prepared runtime is a git checkout whichever way its packages arrived, so
|
|
22
|
+
`legato/src` exists even when legato came from a published wheel. Putting it
|
|
23
|
+
on the path then shadows the installed package with pure Python that has no
|
|
24
|
+
compiled `_legato`, and every kernel dies on `cannot import name
|
|
25
|
+
'get_builder' from 'legato._legato' (unknown location)`.
|
|
26
|
+
|
|
27
|
+
Presence of the built extension is what distinguishes the two layouts, so
|
|
28
|
+
that is what this asks -- no configuration to keep in sync.
|
|
29
|
+
"""
|
|
30
|
+
source = root / "legato" / "src"
|
|
31
|
+
return source if any((source / "legato").glob("_legato*.so")) else None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def launcher_import_paths(kernel_path: Path) -> list[Path]:
|
|
35
|
+
sdk_root = launcher_working_directory()
|
|
36
|
+
compiler = compiler_root()
|
|
37
|
+
candidates = [
|
|
38
|
+
kernel_path.parent,
|
|
39
|
+
# These land ahead of the runtime SDK so its legato is shadowed. They are
|
|
40
|
+
# inserted into sys.path directly, which outranks PYTHONPATH, so setting
|
|
41
|
+
# PYTHONPATH alone would not be enough.
|
|
42
|
+
*([path for path in (legato_source_path(compiler), compiler / "legato_aten_lib") if path] if compiler else []),
|
|
43
|
+
sdk_root,
|
|
44
|
+
*([source] if (source := legato_source_path(sdk_root)) else []),
|
|
45
|
+
]
|
|
46
|
+
paths: list[Path] = []
|
|
47
|
+
seen: set[str] = set()
|
|
48
|
+
for candidate in candidates:
|
|
49
|
+
text = str(candidate.resolve())
|
|
50
|
+
if text in seen:
|
|
51
|
+
continue
|
|
52
|
+
seen.add(text)
|
|
53
|
+
paths.append(candidate)
|
|
54
|
+
return paths
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def script_command(kernel_path: Path) -> list[str]:
|
|
58
|
+
return [sys.executable, str(Path(__file__).with_name("legato_runner_script_wrapper.py")), str(kernel_path)]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def launcher_env(kernel_path: Path, output_root: Path, stages: list[str]) -> dict[str, str]:
|
|
62
|
+
import_paths = launcher_import_paths(kernel_path)
|
|
63
|
+
env = {
|
|
64
|
+
**os.environ,
|
|
65
|
+
"AGORA_LEGATO_OUTPUT_ROOT": str(output_root),
|
|
66
|
+
"AGORA_LEGATO_STAGES": ",".join(stages),
|
|
67
|
+
"LEGATO_OUTPUT_PATH": str(output_root),
|
|
68
|
+
"AGORA_SDK_ROOT": str(launcher_working_directory()),
|
|
69
|
+
"AGORA_LEGATO_IMPORT_PATHS": os.pathsep.join(str(path) for path in import_paths),
|
|
70
|
+
"AGORA_LEGATO_SESSION_LAUNCH": os.environ.get("AGORA_LEGATO_SESSION_LAUNCH", "0"),
|
|
71
|
+
}
|
|
72
|
+
return prepend_pythonpath(env, import_paths)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def prepend_pythonpath(env: dict[str, str], paths: list[Path]) -> dict[str, str]:
|
|
76
|
+
existing = env.get("PYTHONPATH", "")
|
|
77
|
+
values = [str(path) for path in paths]
|
|
78
|
+
values.extend(item for item in existing.split(os.pathsep) if item)
|
|
79
|
+
env["PYTHONPATH"] = os.pathsep.join(dedupe(values))
|
|
80
|
+
return env
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def dedupe(values: list[str]) -> list[str]:
|
|
84
|
+
result: list[str] = []
|
|
85
|
+
seen: set[str] = set()
|
|
86
|
+
for value in values:
|
|
87
|
+
if value in seen:
|
|
88
|
+
continue
|
|
89
|
+
seen.add(value)
|
|
90
|
+
result.append(value)
|
|
91
|
+
return result
|