From 922d01202f389551780fa0fc00578dab3725a559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 8 Oct 2026 02:12:29 -0400 Subject: [PATCH] Run the same checks on Linux that the Windows scripts run The product could only reach a GPU through one hard-coded route: `wsl.exe -d Ubuntu`, a device named "RTX 4090", and launch scripts that existed only as PowerShell. On this machine `WSL_DISTRO_NAME` is already set, so the local shell is Linux and the wsl.exe hop was unnecessary; on any other Linux host it is impossible. Every SGLang, vLLM and KTransformers profile was reported "missing" here even with a working 4090. profiles.py now asks the host what it is instead of assuming: `uses_wsl()` is false inside WSL and only true when wsl.exe is actually on PATH, `host_shell()` runs `bash -lc` directly or through `wsl.exe -d settings.wsl_distro`, `gpu_available()` replaced `wsl_gpu_available()`, and `launch_script()` picks the .sh twin on a Linux host and the .ps1 on a Windows one. The schema field `wsl_gpu` became `gpu_available` because the old name lied on Linux. Training no longer fails on a card name. `cuda_bf16_training()` gates on cuda_available plus bf16_supported, which the training environment already records, so an RTX 5090 or a 4080 passes and a non-BF16 4090 still fails. Fourteen .sh twins cover the six launch profiles plus install, verify, serve, download and finetune; the six WSL-hopping .ps1 files read ELY_EYE_WSL_DISTRO instead of Ubuntu. PowerShell parsing is not verified here because no pwsh exists on this host. ci.yml runs pytest, ruff, go build/vet/test/gofmt, the Vite build and a docker build-plus-curl on every push. To make that lint gate mean anything, the 17 pre-existing ruff findings across backend/src are cleared: 14 mechanical fixes plus three probe handlers that now name the failure families they absorb (OSError, SubprocessError, ValueError) instead of catching Exception. Verification: pytest 239 passed; ruff check backend/src bench clean (17 at HEAD); mypy on the 12 touched files 14 errors, identical at HEAD; pyright on the same files 8 errors, identical at HEAD; bash -n clean on all 15 .sh. --- .github/workflows/ci.yml | 67 ++++++++++++ backend/src/ely_eye/api.py | 6 +- backend/src/ely_eye/cli.py | 2 +- backend/src/ely_eye/config.py | 1 + backend/src/ely_eye/embeddings.py | 1 - backend/src/ely_eye/evidence_graph.py | 2 +- backend/src/ely_eye/profiles.py | 106 +++++++++++-------- backend/src/ely_eye/proofs/checks_runtime.py | 2 +- backend/src/ely_eye/proofs/training_trace.py | 14 +-- backend/src/ely_eye/research_modules.py | 2 +- backend/src/ely_eye/schemas.py | 2 +- backend/src/ely_eye/training.py | 21 ++-- backend/src/ely_eye/verifier.py | 1 - backend/tests/test_host_profiles.py | 102 ++++++++++++++++++ scripts/download-models.sh | 6 ++ scripts/install-cuda-torch.sh | 5 + scripts/install.sh | 14 +++ scripts/launch-ktransformers-extreme.ps1 | 5 +- scripts/launch-ktransformers-extreme.sh | 15 +++ scripts/launch-llamacpp-offline.sh | 20 ++++ scripts/launch-sglang-extreme.ps1 | 5 +- scripts/launch-sglang-extreme.sh | 16 +++ scripts/launch-sglang-live.ps1 | 5 +- scripts/launch-sglang-live.sh | 18 ++++ scripts/launch-vllm-extreme.ps1 | 5 +- scripts/launch-vllm-extreme.sh | 17 +++ scripts/launch-vllm-live.ps1 | 5 +- scripts/launch-vllm-live.sh | 16 +++ scripts/run-prd-finetune.sh | 35 ++++++ scripts/serve-backend.sh | 4 + scripts/serve-frontend.sh | 5 + scripts/setup-linux-runtimes.ps1 | 19 ++-- scripts/setup-linux-runtimes.sh | 20 ++++ scripts/verify.sh | 6 ++ 34 files changed, 482 insertions(+), 88 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 backend/tests/test_host_profiles.py create mode 100755 scripts/download-models.sh create mode 100755 scripts/install-cuda-torch.sh create mode 100755 scripts/install.sh create mode 100755 scripts/launch-ktransformers-extreme.sh create mode 100755 scripts/launch-llamacpp-offline.sh create mode 100755 scripts/launch-sglang-extreme.sh create mode 100755 scripts/launch-sglang-live.sh create mode 100755 scripts/launch-vllm-extreme.sh create mode 100755 scripts/launch-vllm-live.sh create mode 100755 scripts/run-prd-finetune.sh create mode 100755 scripts/serve-backend.sh create mode 100755 scripts/serve-frontend.sh create mode 100755 scripts/setup-linux-runtimes.sh create mode 100755 scripts/verify.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bb0092b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + backend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + - run: pip install -e ./backend + - run: python -m pytest -q + - run: pip install ruff==0.16.10 && ruff check backend/src bench + + mcp: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache-dependency-path: ely-eye-mcp/go.sum + - name: Build, vet and test + working-directory: ely-eye-mcp + run: go build ./... && go vet ./... && go test ./... + - name: gofmt is clean + working-directory: ely-eye-mcp + run: test -z "$(gofmt -l .)" + + interface: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: frontend/package-lock.json + - run: npm ci && npm run build + working-directory: frontend + + image: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build the image from a clean clone + run: docker build -t ely-eye:ci . + - name: The container answers on both faces + run: | + docker run -d --name elyeye -p 8765:8765 ely-eye:ci + url=http://127.0.0.1:8765/api/status + for attempt in $(seq 1 30); do + curl -fsS "$url" -o /tmp/status.json && break + sleep 2 + done + cat /tmp/status.json + curl -fsS http://127.0.0.1:8765/ | grep -q '
' + docker logs elyeye diff --git a/backend/src/ely_eye/api.py b/backend/src/ely_eye/api.py index 080facf..db95213 100644 --- a/backend/src/ely_eye/api.py +++ b/backend/src/ely_eye/api.py @@ -277,8 +277,10 @@ def create_app() -> FastAPI: return benchmark_prediction_report(settings, settings.data_dir / "benchmark_predictions") @app.get("/api/runtime/profiles", response_model=RuntimeProfileReport) - def runtime_profiles() -> RuntimeProfileReport: - return runtime_profile_report() + def runtime_profiles( + settings: Annotated[Settings, Depends(settings_dep)], + ) -> RuntimeProfileReport: + return runtime_profile_report(settings) @app.get("/api/runtime/adapters", response_model=AdapterLoadReport) def runtime_adapters(settings: Annotated[Settings, Depends(settings_dep)]) -> AdapterLoadReport: diff --git a/backend/src/ely_eye/cli.py b/backend/src/ely_eye/cli.py index b432916..4d3787b 100644 --- a/backend/src/ely_eye/cli.py +++ b/backend/src/ely_eye/cli.py @@ -325,7 +325,7 @@ def run_benchmark( @app.command("runtime-profiles") def runtime_profiles() -> None: - typer.echo(runtime_profile_report().model_dump_json(indent=2)) + typer.echo(runtime_profile_report(get_settings()).model_dump_json(indent=2)) @app.command("research-modules") diff --git a/backend/src/ely_eye/config.py b/backend/src/ely_eye/config.py index d39cf30..b556c88 100644 --- a/backend/src/ely_eye/config.py +++ b/backend/src/ely_eye/config.py @@ -27,6 +27,7 @@ class Settings(BaseSettings): sglang_base_url: str = "http://127.0.0.1:8000/v1" sglang_api_key: str = "ely-eye-local" ui_dir: Path | None = None + wsl_distro: str = "Ubuntu" dense_embeddings_enabled: bool = False live_context_tokens: int = 262_144 extreme_context_tokens: int = 1_010_000 diff --git a/backend/src/ely_eye/embeddings.py b/backend/src/ely_eye/embeddings.py index 43e6dfb..fc9af9a 100644 --- a/backend/src/ely_eye/embeddings.py +++ b/backend/src/ely_eye/embeddings.py @@ -2,7 +2,6 @@ from __future__ import annotations import json from dataclasses import dataclass -from pathlib import Path from typing import Any import numpy as np diff --git a/backend/src/ely_eye/evidence_graph.py b/backend/src/ely_eye/evidence_graph.py index 59e88fd..07a5f0b 100644 --- a/backend/src/ely_eye/evidence_graph.py +++ b/backend/src/ely_eye/evidence_graph.py @@ -64,5 +64,5 @@ class EvidenceGraphService: def claim_node_id(question: str, answer: str) -> str: - digest = hashlib.sha256(f"{question}\n{answer}".encode("utf-8")).hexdigest()[:20] + digest = hashlib.sha256(f"{question}\n{answer}".encode()).hexdigest()[:20] return f"claim_{digest}" diff --git a/backend/src/ely_eye/profiles.py b/backend/src/ely_eye/profiles.py index a44e1b0..bc04be7 100644 --- a/backend/src/ely_eye/profiles.py +++ b/backend/src/ely_eye/profiles.py @@ -6,9 +6,9 @@ import shutil import subprocess from pathlib import Path +from .config import Settings from .schemas import RuntimeLaunchProfile, RuntimeProfileReport, RuntimeProfileValidation - EXTREME_YARN_OVERRIDE = { "text_config": { "rope_parameters": { @@ -24,7 +24,7 @@ EXTREME_YARN_OVERRIDE = { } -def runtime_profile_report(project_root: Path | None = None) -> RuntimeProfileReport: +def runtime_profile_report(settings: Settings, project_root: Path | None = None) -> RuntimeProfileReport: root = project_root or Path.cwd() profiles = [ RuntimeLaunchProfile( @@ -185,29 +185,55 @@ def runtime_profile_report(project_root: Path | None = None) -> RuntimeProfileRe required_features=["GGUF", "offline demo", "quantized KV"], ), ] - validations = validate_runtime_profiles(root, profiles) + validations = validate_runtime_profiles(root, profiles, settings) ready = all(validation.status == "ready" for validation in validations) return RuntimeProfileReport( status="ready" if ready else "incomplete", - profiles=profiles, + profiles=[profile.model_copy(update={"script": launch_script(profile.script)}) for profile in profiles], validations=validations, ) +def uses_wsl() -> bool: + """A Windows process reaches Linux through wsl.exe; inside WSL the local shell already is Linux.""" + if os.environ.get("WSL_DISTRO_NAME"): + return False + return shutil.which("wsl.exe") is not None + + +def launch_script(script: str) -> str: + """A Windows host runs the .ps1; a Linux host, WSL included, runs the .sh twin.""" + return script if uses_wsl() else str(Path(script).with_suffix(".sh")) + + +def host_shell(command: str, settings: Settings) -> list[str]: + """Run a shell command where the launch environments live.""" + if uses_wsl(): + return ["wsl.exe", "-d", settings.wsl_distro, "bash", "-lc", command] + return ["bash", "-lc", command] + + +def host_path(root: Path) -> str: + """The path the host shell can cd into; a Windows path is reached through the WSL mount.""" + resolved = root.resolve() + return windows_path_to_wsl(resolved) if uses_wsl() else str(resolved) + + def validate_runtime_profiles( root: Path, profiles: list[RuntimeLaunchProfile], + settings: Settings, ) -> list[RuntimeProfileValidation]: - wsl_gpu = wsl_gpu_available() + gpu = gpu_available(settings) package_cache = { - ".venv-linux": linux_package_versions(root, ".venv-linux"), - ".venv-linux-vllm": linux_package_versions(root, ".venv-linux-vllm"), + ".venv-linux": linux_package_versions(root, ".venv-linux", settings), + ".venv-linux-vllm": linux_package_versions(root, ".venv-linux-vllm", settings), } probe_cache: dict[tuple[str, str, str | None], dict[str, object]] = {} llama_server = resolve_llama_server() validations: list[RuntimeProfileValidation] = [] for profile in profiles: - script_exists = (root / profile.script).exists() + script_exists = (root / launch_script(profile.script)).exists() package = package_for_runtime(profile.runtime) env_name = linux_env_for_runtime(profile.runtime) linux_packages = package_cache.get(env_name, {}) @@ -216,24 +242,24 @@ def validate_runtime_profiles( executable = llama_server if profile.runtime == "llama.cpp" else None probe_key = (profile.runtime, env_name, executable) if probe_key not in probe_cache: - probe_cache[probe_key] = runtime_launcher_probe(root, profile.runtime, env_name, executable) + probe_cache[probe_key] = runtime_launcher_probe(root, profile.runtime, env_name, executable, settings) probe = probe_cache[probe_key] probe_status = str(probe.get("status", "skipped")) probe_passed = probe_status == "passed" if profile.runtime in {"SGLang", "vLLM", "KTransformers"}: - ready = script_exists and bool(package_version) and wsl_gpu and probe_passed + ready = script_exists and bool(package_version) and gpu and probe_passed if profile.runtime == "KTransformers": ready = ready and bool(kt_sglang_version) detail = ( ( - f"WSL GPU ready; {package} {package_version}; " + f"GPU ready; {package} {package_version}; " f"sglang-kt entry {kt_sglang_version}" ) if profile.runtime == "KTransformers" and ready - else f"WSL GPU ready; {package} {package_version}; env {env_name}" + else f"GPU ready; {package} {package_version}; env {env_name}" if ready else ( - f"script={script_exists}, wsl_gpu={wsl_gpu}, env={env_name}, " + f"script={script_exists}, gpu={gpu}, env={env_name}, " f"{package}={package_version}, sglang={kt_sglang_version}" ) ) @@ -253,7 +279,7 @@ def validate_runtime_profiles( package=package, package_version=package_version, executable=executable, - wsl_gpu=wsl_gpu, + gpu_available=gpu, probe_command=[str(item) for item in probe.get("command", [])], probe_status=probe_status, probe_exit_code=probe.get("exit_code") if isinstance(probe.get("exit_code"), int) else None, @@ -278,7 +304,7 @@ def linux_env_for_runtime(runtime: str) -> str: return ".venv-linux" -def linux_package_versions(root: Path, env_name: str) -> dict[str, str]: +def linux_package_versions(root: Path, env_name: str, settings: Settings) -> dict[str, str]: script = ( "import importlib.metadata as md, json;" "names=['sglang','vllm','ktransformers','torch','torchvision','qwen-vl-utils'];" @@ -290,25 +316,21 @@ def linux_package_versions(root: Path, env_name: str) -> dict[str, str]: ) try: output = subprocess.check_output( - [ - "wsl.exe", - "-d", - "Ubuntu", - "bash", - "-lc", - ( - f"cd {wsl_quote(windows_path_to_wsl(root))} " - f"&& test -x {env_name}/bin/python " - f"&& {env_name}/bin/python -c {wsl_quote(script)}" - ), - ], + host_shell( + f"cd {wsl_quote(host_path(root))} " + f"&& test -x {env_name}/bin/python " + f"&& {env_name}/bin/python -c {wsl_quote(script)}", + settings, + ), text=True, stderr=subprocess.DEVNULL, timeout=20, ) - data = json.loads(output.strip()) - return {str(key): str(value) for key, value in data.items()} - except Exception: + payload = json.loads(output.strip()) + if not isinstance(payload, dict): + return {} + return {str(key): str(value) for key, value in payload.items()} + except (OSError, subprocess.SubprocessError, ValueError): return {} @@ -317,6 +339,7 @@ def runtime_launcher_probe( runtime: str, env_name: str, executable: str | None, + settings: Settings, ) -> dict[str, object]: if runtime == "SGLang": probe_script = ( @@ -325,7 +348,7 @@ def runtime_launcher_probe( "print(spec.origin if spec else 'missing'); " "sys.exit(0 if spec else 1)" ) - command = wsl_python_command(root, env_name, f"-c {wsl_quote(probe_script)}") + command = host_python_command(root, env_name, f"-c {wsl_quote(probe_script)}", settings) elif runtime == "vLLM": probe_script = ( "import importlib.util, sys; " @@ -333,10 +356,10 @@ def runtime_launcher_probe( "print(spec.origin if spec else 'missing'); " "sys.exit(0 if spec else 1)" ) - command = wsl_python_command(root, env_name, f"-c {wsl_quote(probe_script)}") + command = host_python_command(root, env_name, f"-c {wsl_quote(probe_script)}", settings) elif runtime == "KTransformers": probe_script = "import ktransformers, sglang; print('ktransformers and sglang import ok')" - command = wsl_python_command(root, env_name, f"-c {wsl_quote(probe_script)}") + command = host_python_command(root, env_name, f"-c {wsl_quote(probe_script)}", settings) elif runtime == "llama.cpp" and executable: command = [executable, "--help"] else: @@ -349,7 +372,7 @@ def runtime_launcher_probe( timeout=30, check=False, ) - except Exception as exc: + except (OSError, subprocess.SubprocessError) as exc: return { "status": "failed", "command": command, @@ -365,13 +388,13 @@ def runtime_launcher_probe( } -def wsl_python_command(root: Path, env_name: str, python_args: str) -> list[str]: +def host_python_command(root: Path, env_name: str, python_args: str, settings: Settings) -> list[str]: shell_command = ( - f"cd {wsl_quote(windows_path_to_wsl(root))} " + f"cd {wsl_quote(host_path(root))} " f"&& test -x {env_name}/bin/python " f"&& {env_name}/bin/python {python_args}" ) - return ["wsl.exe", "-d", "Ubuntu", "bash", "-lc", shell_command] + return host_shell(shell_command, settings) def compact_output(stdout: str, stderr: str, limit: int = 800) -> str: @@ -379,18 +402,17 @@ def compact_output(stdout: str, stderr: str, limit: int = 800) -> str: return text[:limit] -def wsl_gpu_available() -> bool: - if shutil.which("wsl.exe") is None: - return False +def gpu_available(settings: Settings) -> bool: + """Ask the host for a CUDA-capable GPU without naming a card: any GPU the driver lists will do.""" try: output = subprocess.check_output( - ["wsl.exe", "-d", "Ubuntu", "nvidia-smi", "-L"], + host_shell("nvidia-smi -L", settings), text=True, stderr=subprocess.DEVNULL, timeout=10, ) return "GPU" in output - except Exception: + except (OSError, subprocess.SubprocessError): return False diff --git a/backend/src/ely_eye/proofs/checks_runtime.py b/backend/src/ely_eye/proofs/checks_runtime.py index 1cceacf..3a03a77 100644 --- a/backend/src/ely_eye/proofs/checks_runtime.py +++ b/backend/src/ely_eye/proofs/checks_runtime.py @@ -232,7 +232,7 @@ class RuntimeChecks: runtime.unload() def _runtime_profile_check(self, proof_root: Path) -> ProofCheck: - report = runtime_profile_report() + report = runtime_profile_report(self.settings) artifact = proof_root / "runtime_profiles.json" artifact.write_text(report.model_dump_json(indent=2), encoding="utf-8") required_runtimes = {"SGLang", "vLLM", "KTransformers", "llama.cpp"} diff --git a/backend/src/ely_eye/proofs/training_trace.py b/backend/src/ely_eye/proofs/training_trace.py index 5a64b1b..b1c5c3b 100644 --- a/backend/src/ely_eye/proofs/training_trace.py +++ b/backend/src/ely_eye/proofs/training_trace.py @@ -24,6 +24,11 @@ from ..training import ( ) +def cuda_bf16_training(environment: dict[str, Any]) -> bool: + """The requirement is CUDA plus BF16 support, not one card name written down in 2025.""" + return bool(environment.get("cuda_available")) and bool(environment.get("bf16_supported")) + + def count_safetensor_keys(path: Path) -> int: return int(safetensor_weight_stats(path)["tensor_count"]) @@ -173,8 +178,7 @@ def validate_training_trace(manifest: AdapterManifest, adapter_root: Path) -> di } ) environment = summary.get("environment") if isinstance(summary.get("environment"), dict) else {} - device_name = str(environment.get("device_name") or "") - report["cuda_training"] = bool(environment.get("cuda_available")) and "RTX 4090" in device_name + report["cuda_training"] = cuda_bf16_training(environment) report["bf16_supported"] = bool(environment.get("bf16_supported")) report["valid"] = ( report["trace_sha256_ok"] @@ -282,11 +286,7 @@ def validate_training_proof(manifest: AdapterManifest, adapter_root: Path) -> di and int(proof_weight_stats.get("nonzero_elements") or 0) > 0 and bool(proof_weight_stats.get("all_finite")) ), - "proof_cuda_bf16": ( - bool(proof.get("bf16")) - and bool(proof_environment.get("cuda_available")) - and "RTX 4090" in str(proof_environment.get("device_name") or "") - ), + "proof_cuda_bf16": bool(proof.get("bf16")) and cuda_bf16_training(proof_environment), "proof_bf16_supported": bool(proof_environment.get("bf16_supported")), "proof_contract_ok": training_payload_contract_ok(proof, manifest), "proof_optimizer_ok": proof.get("optimizer") == "paged_adamw_8bit", diff --git a/backend/src/ely_eye/research_modules.py b/backend/src/ely_eye/research_modules.py index 1c6a922..21d1e61 100644 --- a/backend/src/ely_eye/research_modules.py +++ b/backend/src/ely_eye/research_modules.py @@ -155,7 +155,7 @@ class ResearchModuleService: "entries": layer.entries, "hit_rate": hit_rate, "evict": hit_rate < 0.25 and layer.entries > 0, - "replay_token": hashlib.sha256(f"{layer.layer}:{layer.entries}".encode("utf-8")).hexdigest()[:16], + "replay_token": hashlib.sha256(f"{layer.layer}:{layer.entries}".encode()).hexdigest()[:16], } ) return {"policy": "hit-rate weighted replay buffer", "layers": rows} diff --git a/backend/src/ely_eye/schemas.py b/backend/src/ely_eye/schemas.py index 6137a6c..3cecd21 100644 --- a/backend/src/ely_eye/schemas.py +++ b/backend/src/ely_eye/schemas.py @@ -439,7 +439,7 @@ class RuntimeProfileValidation(BaseModel): package: str | None = None package_version: str | None = None executable: str | None = None - wsl_gpu: bool = False + gpu_available: bool = False probe_command: list[str] = Field(default_factory=list) probe_status: Literal["passed", "failed", "skipped"] = "skipped" probe_exit_code: int | None = None diff --git a/backend/src/ely_eye/training.py b/backend/src/ely_eye/training.py index 4c81aef..14771da 100644 --- a/backend/src/ely_eye/training.py +++ b/backend/src/ely_eye/training.py @@ -9,7 +9,7 @@ import shutil import subprocess import time from contextlib import contextmanager -from datetime import datetime, timezone +from datetime import UTC, datetime from importlib.metadata import PackageNotFoundError, version from pathlib import Path from typing import Any, TextIO @@ -20,7 +20,6 @@ from .config import Settings, get_settings from .db import Database from .schemas import AdapterManifest, EvidenceAtom, TrainingAdapterKind - SYSTEM_PROMPT = ( "You are Ely-Eye. Answer from supplied evidence, return compact JSON, " "and cite atom ids exactly." @@ -433,7 +432,7 @@ class TrainingService: gradient_accumulation_steps or self.settings.train_gradient_accumulation_steps ) learning_rate = learning_rate or self.settings.train_learning_rate - started_at = datetime.now(timezone.utc) + started_at = datetime.now(UTC) wall_start = time.perf_counter() trace_path = output_dir / "training_trace.jsonl" @@ -575,7 +574,7 @@ class TrainingService: processor.save_pretrained(output_dir) self.write_lora_config(output_dir, kind, rank, alpha, model_id) weight_stats = safetensor_weight_stats(output_dir / "adapter_model.safetensors") - finished_at = datetime.now(timezone.utc) + finished_at = datetime.now(UTC) wall_seconds = time.perf_counter() - wall_start summary_path = output_dir / "training_summary.json" proof_path = output_dir / "training_proof.json" @@ -671,7 +670,7 @@ class TrainingService: manifest = AdapterManifest( adapter_id=adapter_id(kind, dataset_path, model_id), kind=kind, - created_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), proof_version=TRAINING_PROOF_VERSION, training_method=TRAINING_METHOD, precision=TRAINING_PRECISION, @@ -756,7 +755,7 @@ class TrainingService: output_dir.mkdir(parents=True, exist_ok=True) max_length = max_length or self.settings.train_max_length learning_rate = learning_rate or self.settings.train_learning_rate - started_at = datetime.now(timezone.utc) + started_at = datetime.now(UTC) wall_start = time.perf_counter() trace_path = output_dir / "training_trace.jsonl" @@ -882,7 +881,7 @@ class TrainingService: processor.save_pretrained(output_dir) self.write_lora_config(output_dir, TrainingAdapterKind.hme_retrieval, rank, alpha, model_id) weight_stats = safetensor_weight_stats(output_dir / "adapter_model.safetensors") - finished_at = datetime.now(timezone.utc) + finished_at = datetime.now(UTC) wall_seconds = time.perf_counter() - wall_start summary_path = output_dir / "training_summary.json" proof_path = output_dir / "training_proof.json" @@ -977,7 +976,7 @@ class TrainingService: manifest = AdapterManifest( adapter_id=adapter_id(TrainingAdapterKind.hme_retrieval, dataset_path, model_id), kind=TrainingAdapterKind.hme_retrieval, - created_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), proof_version=TRAINING_PROOF_VERSION, training_method=TRAINING_METHOD, precision=TRAINING_PRECISION, @@ -1269,7 +1268,7 @@ def trainable_update_l2(before_update: dict[str, Any], named_parameters: list[tu def require_cuda_bf16(torch_module: Any) -> None: if not torch_module.cuda.is_available(): - raise RuntimeError("PRD 12.2 training requires CUDA BF16 on the local RTX 4090.") + raise RuntimeError("training needs a CUDA device; none is available.") bf16_supported = getattr(torch_module.cuda, "is_bf16_supported", lambda: False) if not bool(bf16_supported()): raise RuntimeError("PRD 12.2 training requires BF16-capable CUDA hardware.") @@ -1598,7 +1597,7 @@ def slugify(value: str) -> str: def adapter_id(kind: TrainingAdapterKind, dataset_path: Path, model_id: str) -> str: - raw = f"{kind.value}:{model_id}:{dataset_path.resolve()}:{datetime.now(timezone.utc).isoformat()}" + raw = f"{kind.value}:{model_id}:{dataset_path.resolve()}:{datetime.now(UTC).isoformat()}" return f"{kind.value}_{sha256_bytes(raw.encode('utf-8'))[:12]}" @@ -1632,7 +1631,7 @@ def sha256_bytes(data: bytes) -> str: class _DigestWriter: - def __init__(self, digest: "hashlib._Hash") -> None: + def __init__(self, digest: hashlib._Hash) -> None: self.digest = digest def write(self, data: bytes) -> int: diff --git a/backend/src/ely_eye/verifier.py b/backend/src/ely_eye/verifier.py index 9447245..1b82b4e 100644 --- a/backend/src/ely_eye/verifier.py +++ b/backend/src/ely_eye/verifier.py @@ -7,7 +7,6 @@ from typing import Any from .schemas import CitationReport, CompiledContext - ATOM_PATTERN = re.compile(r"[A-Za-z0-9_./\\:-]+:(?:text|image|page|frame|region)[A-Za-z0-9_./\\:#=-]*") diff --git a/backend/tests/test_host_profiles.py b/backend/tests/test_host_profiles.py new file mode 100644 index 0000000..ac90410 --- /dev/null +++ b/backend/tests/test_host_profiles.py @@ -0,0 +1,102 @@ +"""The launch layer must describe the machine it runs on, not the one it was written on.""" + +from __future__ import annotations + +import os +import stat +from pathlib import Path + +import pytest + +from ely_eye import profiles +from ely_eye.config import Settings +from ely_eye.proofs.training_trace import cuda_bf16_training +from ely_eye.training import require_cuda_bf16 + + +def _stub_nvidia_smi(tmp_path: Path, monkeypatch, output: str, exit_code: int = 0) -> None: + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + stub = bin_dir / "nvidia-smi" + stub.write_text(f"#!/bin/sh\nprintf '%s\\n' '{output}'\nexit {exit_code}\n", encoding="utf-8") + stub.chmod(stub.stat().st_mode | stat.S_IEXEC) + monkeypatch.setenv("PATH", f"{bin_dir}{os.pathsep}{os.environ['PATH']}") + + +def test_a_gpu_that_is_not_the_written_down_card_still_counts(tmp_path: Path, monkeypatch) -> None: + _stub_nvidia_smi(tmp_path, monkeypatch, "GPU 0: NVIDIA GeForce RTX 5090 (UUID: GPU-1234)") + monkeypatch.setattr(profiles, "uses_wsl", lambda: False) + assert profiles.gpu_available(Settings(home=tmp_path / "home")) is True + + +def test_a_machine_the_driver_cannot_see_reports_no_gpu(tmp_path: Path, monkeypatch) -> None: + _stub_nvidia_smi(tmp_path, monkeypatch, "nvidia-smi: command not found", exit_code=127) + monkeypatch.setattr(profiles, "uses_wsl", lambda: False) + assert profiles.gpu_available(Settings(home=tmp_path / "home")) is False + + +def test_the_distro_name_comes_from_settings(monkeypatch) -> None: + monkeypatch.setattr(profiles, "uses_wsl", lambda: True) + command = profiles.host_shell("true", Settings(home=Path("/tmp"), wsl_distro="Ubuntu-24.04")) + assert command == ["wsl.exe", "-d", "Ubuntu-24.04", "bash", "-lc", "true"] + + +def test_inside_wsl_the_local_shell_runs(monkeypatch, tmp_path: Path) -> None: + monkeypatch.setenv("WSL_DISTRO_NAME", "Ubuntu") + monkeypatch.setattr(profiles.shutil, "which", lambda name: "/mnt/c/WINDOWS/system32/wsl.exe") + assert profiles.uses_wsl() is False + monkeypatch.delenv("WSL_DISTRO_NAME") + assert profiles.uses_wsl() is True + assert profiles.host_shell("true", Settings(home=tmp_path / "home", wsl_distro="Ubuntu"))[0] == "wsl.exe" + + +def test_a_linux_host_is_pointed_at_the_shell_twin(monkeypatch) -> None: + monkeypatch.setattr(profiles, "uses_wsl", lambda: False) + assert profiles.launch_script("scripts/launch-sglang-live.ps1") == "scripts/launch-sglang-live.sh" + monkeypatch.setattr(profiles, "uses_wsl", lambda: True) + assert profiles.launch_script("scripts/launch-sglang-live.ps1") == "scripts/launch-sglang-live.ps1" + + +def test_the_training_verdict_does_not_name_a_card() -> None: + newer = {"cuda_available": True, "bf16_supported": True, "device_name": "NVIDIA GeForce RTX 5090"} + assert cuda_bf16_training(newer) is True + older = {"cuda_available": True, "bf16_supported": True, "device_name": "NVIDIA GeForce RTX 4080"} + assert cuda_bf16_training(older) is True + without_bf16 = {"cuda_available": True, "bf16_supported": False, "device_name": "NVIDIA GeForce RTX 4090"} + assert cuda_bf16_training(without_bf16) is False + + +class _FakeCuda: + def __init__(self, available: bool, bf16: bool) -> None: + self._available = available + self._bf16 = bf16 + + def is_available(self) -> bool: + return self._available + + def is_bf16_supported(self) -> bool: + return self._bf16 + + +class _FakeTorch: + def __init__(self, available: bool, bf16: bool) -> None: + self.cuda = _FakeCuda(available, bf16) + + +def test_training_refuses_a_machine_without_cuda_by_capability_not_card() -> None: + with pytest.raises(RuntimeError) as refused: + require_cuda_bf16(_FakeTorch(available=False, bf16=False)) + assert "4090" not in str(refused.value) + require_cuda_bf16(_FakeTorch(available=True, bf16=True)) + + +def test_every_profile_ships_a_script_for_this_host(tmp_path: Path, monkeypatch) -> None: + root = Path(__file__).resolve().parents[2] + monkeypatch.setattr(profiles, "uses_wsl", lambda: False) + linux = profiles.runtime_profile_report(Settings(home=tmp_path / "home"), root) + assert [item.name for item in linux.validations if not item.script_exists] == [] + assert all(item.script.endswith(".sh") for item in linux.profiles), linux.profiles + monkeypatch.setattr(profiles, "uses_wsl", lambda: True) + windows = profiles.runtime_profile_report(Settings(home=tmp_path / "home"), root) + assert [item.name for item in windows.validations if not item.script_exists] == [] + assert all(item.script.endswith(".ps1") for item in windows.profiles), windows.profiles diff --git a/scripts/download-models.sh b/scripts/download-models.sh new file mode 100755 index 0000000..e7e6759 --- /dev/null +++ b/scripts/download-models.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +for model in Qwen/Qwen3.5-9B Qwen/Qwen3.5-0.8B Qwen/Qwen3-VL-Embedding-8B; do + .venv/bin/python ./scripts/download_models.py --model "$model" +done diff --git a/scripts/install-cuda-torch.sh b/scripts/install-cuda-torch.sh new file mode 100755 index 0000000..96f1c44 --- /dev/null +++ b/scripts/install-cuda-torch.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +.venv/bin/python -m pip install --upgrade --no-cache-dir --index-url https://download.pytorch.org/whl/cu130 torch==2.12.0+cu130 torchvision==0.27.0+cu130 +.venv/bin/python -c 'import torch; print(torch.__version__); print(torch.cuda.is_available()); print(torch.version.cuda)' diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..8eb38c7 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +if [ ! -d .venv ]; then + python3 -m venv .venv +fi +.venv/bin/python -m pip install --upgrade pip setuptools wheel +.venv/bin/python -m pip install -e ./backend +.venv/bin/python -m pip install --upgrade --no-cache-dir unsloth unsloth_zoo +.venv/bin/python -m pip install --upgrade --no-cache-dir --index-url https://download.pytorch.org/whl/cu130 torch==2.12.0+cu130 torchvision==0.27.0+cu130 +.venv/bin/python -m pip install --upgrade --no-cache-dir transformers==5.9.0 datasets==4.8.5 dill==0.4.1 multiprocess==0.70.19 fsspec==2026.2.0 safetensors==0.8.0rc0 +(cd frontend && npm install) +[ -f .env ] || cp .env.example .env +.venv/bin/ely-eye status diff --git a/scripts/launch-ktransformers-extreme.ps1 b/scripts/launch-ktransformers-extreme.ps1 index e6ae00b..914d48d 100644 --- a/scripts/launch-ktransformers-extreme.ps1 +++ b/scripts/launch-ktransformers-extreme.ps1 @@ -1,12 +1,13 @@ $ErrorActionPreference = "Stop" Set-Location (Split-Path -Parent $PSScriptRoot) +$distro = if ($env:ELY_EYE_WSL_DISTRO) { $env:ELY_EYE_WSL_DISTRO } else { "Ubuntu" } if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) { throw "WSL is required for the KTransformers Linux runtime." } -$wslRoot = (wsl.exe -d Ubuntu wslpath -a (Get-Location).Path).Trim() +$wslRoot = (wsl.exe -d $distro wslpath -a (Get-Location).Path).Trim() $command = @" set -euo pipefail cd "$wslRoot" @@ -20,4 +21,4 @@ python -m sglang.launch_server \ --cpu-offload-gb 48 "@ -wsl.exe -d Ubuntu -- bash -lc $command +wsl.exe -d $distro -- bash -lc $command diff --git a/scripts/launch-ktransformers-extreme.sh b/scripts/launch-ktransformers-extreme.sh new file mode 100755 index 0000000..4d925d6 --- /dev/null +++ b/scripts/launch-ktransformers-extreme.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +if [ ! -f .venv-linux/bin/activate ]; then + echo "Run scripts/setup-linux-runtimes.sh before launching SGLang." >&2 + exit 1 +fi +source .venv-linux/bin/activate +export SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1 +exec python -m sglang.launch_server \ + --model-path Qwen/Qwen3.5-9B \ + --port 8002 \ + --context-length 1010000 \ + --kt-cpuinfer 16 \ + --cpu-offload-gb 48 diff --git a/scripts/launch-llamacpp-offline.sh b/scripts/launch-llamacpp-offline.sh new file mode 100755 index 0000000..b9ff08f --- /dev/null +++ b/scripts/launch-llamacpp-offline.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +if [ -z "${ELY_EYE_GGUF_MODEL:-}" ]; then + echo "Set ELY_EYE_GGUF_MODEL to the absolute GGUF model path." >&2 + exit 1 +fi +server="${LLAMA_CPP_SERVER:-llama-server}" +if ! command -v "$server" >/dev/null 2>&1; then + echo "llama-server executable is missing. Build llama.cpp or set LLAMA_CPP_SERVER." >&2 + exit 1 +fi +exec "$server" \ + --model "$ELY_EYE_GGUF_MODEL" \ + --host 127.0.0.1 \ + --port 8003 \ + --ctx-size 262144 \ + --flash-attn \ + --cache-type-k q4_0 \ + --cache-type-v q4_0 diff --git a/scripts/launch-sglang-extreme.ps1 b/scripts/launch-sglang-extreme.ps1 index f52fc61..beff646 100644 --- a/scripts/launch-sglang-extreme.ps1 +++ b/scripts/launch-sglang-extreme.ps1 @@ -1,13 +1,14 @@ $ErrorActionPreference = "Stop" Set-Location (Split-Path -Parent $PSScriptRoot) +$distro = if ($env:ELY_EYE_WSL_DISTRO) { $env:ELY_EYE_WSL_DISTRO } else { "Ubuntu" } if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) { throw "WSL is required for the SGLang Linux runtime." } $override = '{"text_config":{"rope_parameters":{"mrope_interleaved":true,"mrope_section":[11,11,10],"rope_type":"yarn","rope_theta":10000000,"partial_rotary_factor":0.25,"factor":4.0,"original_max_position_embeddings":262144}}}' -$wslRoot = (wsl.exe -d Ubuntu wslpath -a (Get-Location).Path).Trim() +$wslRoot = (wsl.exe -d $distro wslpath -a (Get-Location).Path).Trim() $command = @" set -euo pipefail cd "$wslRoot" @@ -22,4 +23,4 @@ python -m sglang.launch_server \ --json-model-override-args '$override' "@ -wsl.exe -d Ubuntu -- bash -lc $command +wsl.exe -d $distro -- bash -lc $command diff --git a/scripts/launch-sglang-extreme.sh b/scripts/launch-sglang-extreme.sh new file mode 100755 index 0000000..fabfc6a --- /dev/null +++ b/scripts/launch-sglang-extreme.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +if [ ! -f .venv-linux/bin/activate ]; then + echo "Run scripts/setup-linux-runtimes.sh before launching SGLang." >&2 + exit 1 +fi +source .venv-linux/bin/activate +export SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1 +exec python -m sglang.launch_server \ + --model-path Qwen/Qwen3.5-9B \ + --port 8000 \ + --tp-size 1 \ + --mem-fraction-static 0.88 \ + --context-length 1010000 \ + --json-model-override-args '{"text_config":{"rope_parameters":{"mrope_interleaved":true,"mrope_section":[11,11,10],"rope_type":"yarn","rope_theta":10000000,"partial_rotary_factor":0.25,"factor":4.0,"original_max_position_embeddings":262144}}}' diff --git a/scripts/launch-sglang-live.ps1 b/scripts/launch-sglang-live.ps1 index c13072b..b344cb4 100644 --- a/scripts/launch-sglang-live.ps1 +++ b/scripts/launch-sglang-live.ps1 @@ -1,12 +1,13 @@ $ErrorActionPreference = "Stop" Set-Location (Split-Path -Parent $PSScriptRoot) +$distro = if ($env:ELY_EYE_WSL_DISTRO) { $env:ELY_EYE_WSL_DISTRO } else { "Ubuntu" } if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) { throw "WSL is required for the SGLang Linux runtime." } -$wslRoot = (wsl.exe -d Ubuntu wslpath -a (Get-Location).Path).Trim() +$wslRoot = (wsl.exe -d $distro wslpath -a (Get-Location).Path).Trim() $command = @" set -euo pipefail cd "$wslRoot" @@ -23,4 +24,4 @@ python -m sglang.launch_server \ --speculative-num-draft-tokens 4 "@ -wsl.exe -d Ubuntu -- bash -lc $command +wsl.exe -d $distro -- bash -lc $command diff --git a/scripts/launch-sglang-live.sh b/scripts/launch-sglang-live.sh new file mode 100755 index 0000000..0ced613 --- /dev/null +++ b/scripts/launch-sglang-live.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +if [ ! -f .venv-linux/bin/activate ]; then + echo "Run scripts/setup-linux-runtimes.sh before launching SGLang." >&2 + exit 1 +fi +source .venv-linux/bin/activate +exec python -m sglang.launch_server \ + --model-path Qwen/Qwen3.5-9B \ + --port 8000 \ + --tp-size 1 \ + --mem-fraction-static 0.82 \ + --context-length 262144 \ + --reasoning-parser qwen3 \ + --speculative-algo NEXTN \ + --speculative-num-steps 3 \ + --speculative-num-draft-tokens 4 diff --git a/scripts/launch-vllm-extreme.ps1 b/scripts/launch-vllm-extreme.ps1 index 5e83722..33c382d 100644 --- a/scripts/launch-vllm-extreme.ps1 +++ b/scripts/launch-vllm-extreme.ps1 @@ -1,12 +1,13 @@ $ErrorActionPreference = "Stop" Set-Location (Split-Path -Parent $PSScriptRoot) +$distro = if ($env:ELY_EYE_WSL_DISTRO) { $env:ELY_EYE_WSL_DISTRO } else { "Ubuntu" } if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) { throw "WSL is required for the vLLM Linux runtime." } -$wslRoot = (wsl.exe -d Ubuntu wslpath -a (Get-Location).Path).Trim() +$wslRoot = (wsl.exe -d $distro wslpath -a (Get-Location).Path).Trim() $ropeScaling = '{"rope_type":"yarn","factor":4.0,"original_max_position_embeddings":262144}' $command = @" set -euo pipefail @@ -23,4 +24,4 @@ python -m vllm.entrypoints.openai.api_server \ --rope-scaling '$ropeScaling' "@ -wsl.exe -d Ubuntu -- bash -lc $command +wsl.exe -d $distro -- bash -lc $command diff --git a/scripts/launch-vllm-extreme.sh b/scripts/launch-vllm-extreme.sh new file mode 100755 index 0000000..d26d4d3 --- /dev/null +++ b/scripts/launch-vllm-extreme.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +if [ ! -f .venv-linux-vllm/bin/activate ]; then + echo "Run scripts/setup-linux-runtimes.sh before launching vLLM." >&2 + exit 1 +fi +source .venv-linux-vllm/bin/activate +exec python -m vllm.entrypoints.openai.api_server \ + --model Qwen/Qwen3.5-9B \ + --host 127.0.0.1 \ + --port 8001 \ + --max-model-len 1010000 \ + --enable-prefix-caching \ + --gpu-memory-utilization 0.88 \ + --trust-remote-code \ + --rope-scaling '{"rope_type":"yarn","factor":4.0,"original_max_position_embeddings":262144}' diff --git a/scripts/launch-vllm-live.ps1 b/scripts/launch-vllm-live.ps1 index 2562544..65b7968 100644 --- a/scripts/launch-vllm-live.ps1 +++ b/scripts/launch-vllm-live.ps1 @@ -1,12 +1,13 @@ $ErrorActionPreference = "Stop" Set-Location (Split-Path -Parent $PSScriptRoot) +$distro = if ($env:ELY_EYE_WSL_DISTRO) { $env:ELY_EYE_WSL_DISTRO } else { "Ubuntu" } if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) { throw "WSL is required for the vLLM Linux runtime." } -$wslRoot = (wsl.exe -d Ubuntu wslpath -a (Get-Location).Path).Trim() +$wslRoot = (wsl.exe -d $distro wslpath -a (Get-Location).Path).Trim() $command = @" set -euo pipefail cd "$wslRoot" @@ -21,4 +22,4 @@ python -m vllm.entrypoints.openai.api_server \ --trust-remote-code "@ -wsl.exe -d Ubuntu -- bash -lc $command +wsl.exe -d $distro -- bash -lc $command diff --git a/scripts/launch-vllm-live.sh b/scripts/launch-vllm-live.sh new file mode 100755 index 0000000..eb8e42f --- /dev/null +++ b/scripts/launch-vllm-live.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +if [ ! -f .venv-linux-vllm/bin/activate ]; then + echo "Run scripts/setup-linux-runtimes.sh before launching vLLM." >&2 + exit 1 +fi +source .venv-linux-vllm/bin/activate +exec python -m vllm.entrypoints.openai.api_server \ + --model Qwen/Qwen3.5-9B \ + --host 127.0.0.1 \ + --port 8001 \ + --max-model-len 262144 \ + --enable-prefix-caching \ + --gpu-memory-utilization 0.82 \ + --trust-remote-code diff --git a/scripts/run-prd-finetune.sh b/scripts/run-prd-finetune.sh new file mode 100755 index 0000000..a93bd14 --- /dev/null +++ b/scripts/run-prd-finetune.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +max_steps="${MAX_STEPS:-37}" +rank="${RANK:-16}" +alpha="${ALPHA:-32}" +max_length="${MAX_LENGTH:-8192}" +gradient_accumulation_steps="${GRADIENT_ACCUMULATION_STEPS:-4}" +cartridge_id="${CARTRIDGE_ID:-}" + +dataset=.ely_eye/training/hme_prd_sft.jsonl +adapter=.ely_eye/adapters/hme_core_lora_qwen35_9b + +.venv/bin/ely-eye build-prd-training-data \ + --prd-path ./PRD.md \ + --output-path "$dataset" \ + --max-sections 48 + +args=(train-lora "$dataset" "$adapter" + --max-steps "$max_steps" + --kind hme_core_lora + --rank "$rank" + --alpha "$alpha" + --max-length "$max_length" + --gradient-accumulation-steps "$gradient_accumulation_steps") + +if [ -n "$cartridge_id" ]; then + args+=(--cartridge-id "$cartridge_id") +fi + +.venv/bin/ely-eye "${args[@]}" + +echo "Adapter ready: $adapter" +echo "Use with: ELY_EYE_ADAPTER_DIR=$adapter" diff --git a/scripts/serve-backend.sh b/scripts/serve-backend.sh new file mode 100755 index 0000000..567949c --- /dev/null +++ b/scripts/serve-backend.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +exec .venv/bin/ely-eye serve --host 127.0.0.1 --port 8765 diff --git a/scripts/serve-frontend.sh b/scripts/serve-frontend.sh new file mode 100755 index 0000000..85b1c17 --- /dev/null +++ b/scripts/serve-frontend.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +cd frontend +exec npm run dev diff --git a/scripts/setup-linux-runtimes.ps1 b/scripts/setup-linux-runtimes.ps1 index 7f34d85..b68e619 100644 --- a/scripts/setup-linux-runtimes.ps1 +++ b/scripts/setup-linux-runtimes.ps1 @@ -1,20 +1,21 @@ $ErrorActionPreference = "Stop" Set-Location (Split-Path -Parent $PSScriptRoot) +$distro = if ($env:ELY_EYE_WSL_DISTRO) { $env:ELY_EYE_WSL_DISTRO } else { "Ubuntu" } if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) { throw "WSL is required for SGLang, vLLM, and KTransformers runtime profiles." } -wsl.exe -d Ubuntu -- bash -lc "command -v nvidia-smi >/dev/null && nvidia-smi -L >/dev/null" -wsl.exe -d Ubuntu -- bash -lc "command -v curl >/dev/null || (apt-get update && apt-get install -y curl)" -wsl.exe -d Ubuntu -- bash -lc "command -v /root/.local/bin/uv >/dev/null || curl -LsSf https://astral.sh/uv/install.sh | sh" -$wslRoot = (wsl.exe -d Ubuntu wslpath -a (Get-Location).Path).Trim() -wsl.exe -d Ubuntu -- bash -lc "cd '$wslRoot' && /root/.local/bin/uv venv .venv-linux --python 3.12" -wsl.exe -d Ubuntu -- bash -lc "cd '$wslRoot' && /root/.local/bin/uv venv .venv-linux-vllm --python 3.12" -wsl.exe -d Ubuntu -- bash -lc "cd '$wslRoot' && source .venv-linux/bin/activate && /root/.local/bin/uv pip install --prerelease=allow -r backend/requirements-sglang-linux.txt" -wsl.exe -d Ubuntu -- bash -lc "cd '$wslRoot' && source .venv-linux-vllm/bin/activate && /root/.local/bin/uv pip install --prerelease=allow -r backend/requirements-vllm-linux.txt" -wsl.exe -d Ubuntu -- bash -lc "cd '$wslRoot' && source .venv-linux/bin/activate && /root/.local/bin/uv pip install --prerelease=allow -r backend/requirements-ktransformers-linux.txt" +wsl.exe -d $distro -- bash -lc "command -v nvidia-smi >/dev/null && nvidia-smi -L >/dev/null" +wsl.exe -d $distro -- bash -lc "command -v curl >/dev/null || (apt-get update && apt-get install -y curl)" +wsl.exe -d $distro -- bash -lc "command -v /root/.local/bin/uv >/dev/null || curl -LsSf https://astral.sh/uv/install.sh | sh" +$wslRoot = (wsl.exe -d $distro wslpath -a (Get-Location).Path).Trim() +wsl.exe -d $distro -- bash -lc "cd '$wslRoot' && /root/.local/bin/uv venv .venv-linux --python 3.12" +wsl.exe -d $distro -- bash -lc "cd '$wslRoot' && /root/.local/bin/uv venv .venv-linux-vllm --python 3.12" +wsl.exe -d $distro -- bash -lc "cd '$wslRoot' && source .venv-linux/bin/activate && /root/.local/bin/uv pip install --prerelease=allow -r backend/requirements-sglang-linux.txt" +wsl.exe -d $distro -- bash -lc "cd '$wslRoot' && source .venv-linux-vllm/bin/activate && /root/.local/bin/uv pip install --prerelease=allow -r backend/requirements-vllm-linux.txt" +wsl.exe -d $distro -- bash -lc "cd '$wslRoot' && source .venv-linux/bin/activate && /root/.local/bin/uv pip install --prerelease=allow -r backend/requirements-ktransformers-linux.txt" if (-not (Get-Command llama-server -ErrorAction SilentlyContinue)) { winget install --id ggml.llamacpp -e --accept-package-agreements --accept-source-agreements diff --git a/scripts/setup-linux-runtimes.sh b/scripts/setup-linux-runtimes.sh new file mode 100755 index 0000000..7b6f2a6 --- /dev/null +++ b/scripts/setup-linux-runtimes.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +if ! command -v nvidia-smi >/dev/null 2>&1; then + echo "nvidia-smi is missing; these runtimes need a CUDA host." >&2 + exit 1 +fi +nvidia-smi -L >/dev/null +uv_bin="${ELY_EYE_UV:-$HOME/.local/bin/uv}" +if ! command -v "$uv_bin" >/dev/null 2>&1; then + curl -LsSf https://astral.sh/uv/install.sh | sh + uv_bin="$HOME/.local/bin/uv" +fi +for venv in .venv-linux .venv-linux-vllm; do + "$uv_bin" venv "$venv" --python 3.12 +done +"$uv_bin" pip install --python .venv-linux/bin/python --prerelease=allow -r backend/requirements-sglang-linux.txt +"$uv_bin" pip install --python .venv-linux-vllm/bin/python --prerelease=allow -r backend/requirements-vllm-linux.txt +"$uv_bin" pip install --python .venv-linux/bin/python --prerelease=allow -r backend/requirements-ktransformers-linux.txt +.venv/bin/ely-eye runtime-profiles diff --git a/scripts/verify.sh b/scripts/verify.sh new file mode 100755 index 0000000..0dbdc4b --- /dev/null +++ b/scripts/verify.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +.venv/bin/python -m compileall ./backend/src +.venv/bin/ely-eye status +(cd frontend && npm run build)