Deploy cold-start reliability update (source: 85cf4fa)
Browse filesBatch ingestion + serve-fast warmup + runtime healthcheck tuning
- .gitignore +2 -0
- Dockerfile +2 -2
- README.md +31 -0
- demo.py +345 -21
.gitignore
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
dataset_build/
|
| 2 |
dataset_build_smoke/
|
| 3 |
assets_runtime_smoke/
|
|
|
|
| 1 |
+
assets/*
|
| 2 |
+
!assets/.gitkeep
|
| 3 |
dataset_build/
|
| 4 |
dataset_build_smoke/
|
| 5 |
assets_runtime_smoke/
|
Dockerfile
CHANGED
|
@@ -32,7 +32,7 @@ COPY --chown=user assets ./assets
|
|
| 32 |
|
| 33 |
EXPOSE 7860
|
| 34 |
|
| 35 |
-
HEALTHCHECK --interval=30s --timeout=10s --start-period=
|
| 36 |
-
CMD curl -f http://
|
| 37 |
|
| 38 |
CMD ["python", "demo.py"]
|
|
|
|
| 32 |
|
| 33 |
EXPOSE 7860
|
| 34 |
|
| 35 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=180s --retries=10 \
|
| 36 |
+
CMD sh -c "curl -f http://127.0.0.1:${PORT:-7860}/__hyperview__/health || exit 1"
|
| 37 |
|
| 38 |
CMD ["python", "demo.py"]
|
README.md
CHANGED
|
@@ -27,6 +27,10 @@ Runtime environment variables:
|
|
| 27 |
- `HF_DATASET_SPLIT` (default: `train`)
|
| 28 |
- `EMBEDDING_ASSET_DIR` (default: `./assets`)
|
| 29 |
- `EMBEDDING_ASSET_MANIFEST` (default: `${EMBEDDING_ASSET_DIR}/manifest.json`)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
- `HYPERVIEW_DEFAULT_PANEL` (default: `spherical3d`; enables Sphere 3D as initial scatter panel)
|
| 31 |
- `HYPERVIEW_LAYOUT_CACHE_VERSION` (default: `v6`; bumps dock layout localStorage key to invalidate stale cached panel state)
|
| 32 |
- `HYPERVIEW_BIND_HOST` (preferred bind host; optional)
|
|
@@ -40,6 +44,33 @@ On Hugging Face Spaces, `SPACE_HOST` may be injected as `<space-subdomain>.hf.sp
|
|
| 40 |
|
| 41 |
The runtime also patches HyperView's dock-layout cache key from legacy `hyperview:dockview-layout:v5` to `hyperview:dockview-layout:${HYPERVIEW_LAYOUT_CACHE_VERSION}` to force migration away from stale panel layouts after UI/layout changes. For future migrations, increment `HYPERVIEW_LAYOUT_CACHE_VERSION` (for example, `v7`) without changing code.
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
## Important Note
|
| 44 |
|
| 45 |
HyperView similarity search currently uses cosine distance in storage backends. The Lorentz panel in this Space is intended for embedding-space visualization and geometry-aware comparison rather than canonical Lorentz-distance retrieval scoring.
|
|
|
|
| 27 |
- `HF_DATASET_SPLIT` (default: `train`)
|
| 28 |
- `EMBEDDING_ASSET_DIR` (default: `./assets`)
|
| 29 |
- `EMBEDDING_ASSET_MANIFEST` (default: `${EMBEDDING_ASSET_DIR}/manifest.json`)
|
| 30 |
+
- `HYPERVIEW_STARTUP_MODE` (default: `serve_fast`; choices: `serve_fast|blocking`)
|
| 31 |
+
- `HYPERVIEW_WARMUP_STATUS_PATH` (default: `/tmp/hyperview_warmup_status.json`)
|
| 32 |
+
- `HYPERVIEW_WARMUP_FAILURE_POLICY` (default: `exit`; choices: `exit|warn`)
|
| 33 |
+
- `HYPERVIEW_BATCH_INSERT_SIZE` (default: `500`; controls sample-batch insertion chunk size)
|
| 34 |
- `HYPERVIEW_DEFAULT_PANEL` (default: `spherical3d`; enables Sphere 3D as initial scatter panel)
|
| 35 |
- `HYPERVIEW_LAYOUT_CACHE_VERSION` (default: `v6`; bumps dock layout localStorage key to invalidate stale cached panel state)
|
| 36 |
- `HYPERVIEW_BIND_HOST` (preferred bind host; optional)
|
|
|
|
| 44 |
|
| 45 |
The runtime also patches HyperView's dock-layout cache key from legacy `hyperview:dockview-layout:v5` to `hyperview:dockview-layout:${HYPERVIEW_LAYOUT_CACHE_VERSION}` to force migration away from stale panel layouts after UI/layout changes. For future migrations, increment `HYPERVIEW_LAYOUT_CACHE_VERSION` (for example, `v7`) without changing code.
|
| 46 |
|
| 47 |
+
## Startup and Warmup Semantics
|
| 48 |
+
|
| 49 |
+
- `HYPERVIEW_STARTUP_MODE=serve_fast` (default):
|
| 50 |
+
- Starts the HyperView server immediately.
|
| 51 |
+
- Runs dataset warmup asynchronously in a background thread.
|
| 52 |
+
- Warmup phases are persisted as JSON: `ingest -> spaces -> layouts -> ready`.
|
| 53 |
+
- `HYPERVIEW_STARTUP_MODE=blocking`:
|
| 54 |
+
- Performs warmup synchronously before serving traffic.
|
| 55 |
+
|
| 56 |
+
Warmup status JSON fields include:
|
| 57 |
+
|
| 58 |
+
- `status` (`starting|running|ready|failed`)
|
| 59 |
+
- `phase` (`boot|ingest|spaces|layouts|ready|failed`)
|
| 60 |
+
- `counts` (sample/space/layout counters and ingestion stats)
|
| 61 |
+
- `error` (exception payload when warmup fails)
|
| 62 |
+
- `timestamps` (`started_at`, `updated_at`, plus terminal timestamps)
|
| 63 |
+
|
| 64 |
+
Failure policy behavior:
|
| 65 |
+
|
| 66 |
+
- `HYPERVIEW_WARMUP_FAILURE_POLICY=exit` (default): process exits on warmup failure.
|
| 67 |
+
- `HYPERVIEW_WARMUP_FAILURE_POLICY=warn`: process stays up and records failure in warmup status JSON.
|
| 68 |
+
|
| 69 |
+
Healthcheck semantics:
|
| 70 |
+
|
| 71 |
+
- Container health (`/__hyperview__/health`) indicates server liveness only.
|
| 72 |
+
- Data readiness (dataset/spaces/layouts completed) is indicated by warmup status JSON (`status=ready`).
|
| 73 |
+
|
| 74 |
## Important Note
|
| 75 |
|
| 76 |
HyperView similarity search currently uses cosine distance in storage backends. The Lorentz panel in this Space is intended for embedding-space visualization and geometry-aware comparison rather than canonical Lorentz-distance retrieval scoring.
|
demo.py
CHANGED
|
@@ -6,6 +6,10 @@ from __future__ import annotations
|
|
| 6 |
import json
|
| 7 |
import os
|
| 8 |
import re
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
from pathlib import Path
|
| 10 |
from typing import Any
|
| 11 |
|
|
@@ -14,6 +18,7 @@ from datasets import Dataset as HFDataset
|
|
| 14 |
from datasets import DatasetDict as HFDatasetDict
|
| 15 |
from datasets import load_dataset, load_from_disk
|
| 16 |
import hyperview as hv
|
|
|
|
| 17 |
from hyperview.core.sample import Sample
|
| 18 |
|
| 19 |
SPACE_HOST = os.environ.get("SPACE_HOST", "0.0.0.0")
|
|
@@ -34,6 +39,113 @@ ASSET_MANIFEST_PATH = Path(
|
|
| 34 |
os.environ.get("EMBEDDING_ASSET_MANIFEST", str((EMBEDDING_ASSET_DIR / "manifest.json").resolve()))
|
| 35 |
)
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
def _patch_hyperview_default_panel() -> None:
|
| 39 |
"""Patch HyperView 0.3.1 frontend for default panel and dock cache-key migration.
|
|
@@ -169,45 +281,103 @@ def _load_hf_rows() -> HFDataset:
|
|
| 169 |
return load_dataset(HF_DATASET_REPO, name=HF_DATASET_CONFIG, split=HF_DATASET_SPLIT)
|
| 170 |
|
| 171 |
|
| 172 |
-
def ingest_hf_dataset_samples(dataset: hv.Dataset) ->
|
| 173 |
rows = _load_hf_rows()
|
| 174 |
media_root = Path(os.environ.get("HYPERVIEW_MEDIA_DIR", "./demo_data/media")) / DATASET_NAME
|
| 175 |
media_root.mkdir(parents=True, exist_ok=True)
|
| 176 |
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
for index, row in enumerate(rows):
|
| 179 |
filename = str(row.get("filename", f"sample_{index:06d}.jpg"))
|
| 180 |
sample_id = str(row.get("sample_id", filename))
|
| 181 |
-
if
|
|
|
|
| 182 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
image_obj = row["image"]
|
| 185 |
image_path = media_root / f"{Path(sample_id).stem}.jpg"
|
| 186 |
if not image_path.exists():
|
| 187 |
image_obj.convert("RGB").save(image_path, format="JPEG", quality=90, optimize=True)
|
|
|
|
| 188 |
|
| 189 |
-
label = str(row.get("label", ""))
|
| 190 |
metadata = {
|
| 191 |
"filename": filename,
|
| 192 |
"sample_id": sample_id,
|
| 193 |
-
"split_tag": str(
|
| 194 |
-
"identity": label,
|
| 195 |
"source_repo": HF_DATASET_REPO,
|
| 196 |
"source_config": HF_DATASET_CONFIG,
|
| 197 |
"source_split": HF_DATASET_SPLIT,
|
| 198 |
}
|
| 199 |
|
| 200 |
-
|
| 201 |
Sample(
|
| 202 |
id=sample_id,
|
| 203 |
filepath=str(image_path),
|
| 204 |
-
label=label,
|
| 205 |
metadata=metadata,
|
| 206 |
)
|
| 207 |
)
|
| 208 |
-
added += 1
|
| 209 |
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
|
| 213 |
def ensure_embedding_spaces(dataset: hv.Dataset, asset_manifest: dict[str, Any], asset_dir: Path) -> None:
|
|
@@ -280,46 +450,200 @@ def ensure_layouts(dataset: hv.Dataset, asset_manifest: dict[str, Any]) -> list[
|
|
| 280 |
return layout_keys
|
| 281 |
|
| 282 |
|
| 283 |
-
def
|
| 284 |
asset_manifest = load_asset_manifest(ASSET_MANIFEST_PATH)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
-
|
| 287 |
if len(dataset) == 0:
|
| 288 |
print(
|
| 289 |
f"Loading HF dataset rows from {HF_DATASET_REPO}[{HF_DATASET_CONFIG}] split={HF_DATASET_SPLIT}"
|
| 290 |
)
|
| 291 |
-
ingest_hf_dataset_samples(dataset)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
ensure_embedding_spaces(dataset, asset_manifest=asset_manifest, asset_dir=EMBEDDING_ASSET_DIR)
|
|
|
|
|
|
|
|
|
|
| 294 |
layout_keys = ensure_layouts(dataset, asset_manifest=asset_manifest)
|
| 295 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
print(f"Dataset '{DATASET_NAME}' has {len(dataset)} samples")
|
| 297 |
print(f"Spaces: {[space.space_key for space in dataset.list_spaces()]}")
|
| 298 |
print(f"Layouts: {layout_keys}")
|
| 299 |
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
|
| 303 |
def main() -> None:
|
| 304 |
_patch_hyperview_default_panel()
|
| 305 |
-
dataset = build_dataset()
|
| 306 |
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
|
| 311 |
bind_host, bind_warning = _resolve_bind_host()
|
| 312 |
bind_port = _resolve_port()
|
| 313 |
|
| 314 |
if bind_warning:
|
| 315 |
print(f"Bind host notice: {bind_warning}")
|
|
|
|
| 316 |
print(
|
| 317 |
-
"Starting HyperView with "
|
| 318 |
-
f"
|
|
|
|
| 319 |
f"(SPACE_HOST={SPACE_HOST!r}, SPACE_PORT={os.environ.get('SPACE_PORT')!r}, "
|
| 320 |
f"PORT={os.environ.get('PORT')!r})"
|
| 321 |
)
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
|
| 324 |
|
| 325 |
if __name__ == "__main__":
|
|
|
|
| 6 |
import json
|
| 7 |
import os
|
| 8 |
import re
|
| 9 |
+
import threading
|
| 10 |
+
import time
|
| 11 |
+
import traceback
|
| 12 |
+
from datetime import datetime, timezone
|
| 13 |
from pathlib import Path
|
| 14 |
from typing import Any
|
| 15 |
|
|
|
|
| 18 |
from datasets import DatasetDict as HFDatasetDict
|
| 19 |
from datasets import load_dataset, load_from_disk
|
| 20 |
import hyperview as hv
|
| 21 |
+
from hyperview.api import Session
|
| 22 |
from hyperview.core.sample import Sample
|
| 23 |
|
| 24 |
SPACE_HOST = os.environ.get("SPACE_HOST", "0.0.0.0")
|
|
|
|
| 39 |
os.environ.get("EMBEDDING_ASSET_MANIFEST", str((EMBEDDING_ASSET_DIR / "manifest.json").resolve()))
|
| 40 |
)
|
| 41 |
|
| 42 |
+
DEFAULT_STARTUP_MODE = "serve_fast"
|
| 43 |
+
DEFAULT_FAILURE_POLICY = "exit"
|
| 44 |
+
DEFAULT_BATCH_INSERT_SIZE = 500
|
| 45 |
+
DEFAULT_WARMUP_STATUS_PATH = Path("/tmp/hyperview_warmup_status.json")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _utc_now() -> str:
|
| 49 |
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _resolve_startup_mode() -> str:
|
| 53 |
+
startup_mode = os.environ.get("HYPERVIEW_STARTUP_MODE", DEFAULT_STARTUP_MODE).strip().lower()
|
| 54 |
+
if startup_mode in {"serve_fast", "blocking"}:
|
| 55 |
+
return startup_mode
|
| 56 |
+
print(
|
| 57 |
+
f"Invalid HYPERVIEW_STARTUP_MODE={startup_mode!r}; "
|
| 58 |
+
f"falling back to {DEFAULT_STARTUP_MODE!r}."
|
| 59 |
+
)
|
| 60 |
+
return DEFAULT_STARTUP_MODE
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _resolve_failure_policy() -> str:
|
| 64 |
+
failure_policy = os.environ.get("HYPERVIEW_WARMUP_FAILURE_POLICY", DEFAULT_FAILURE_POLICY).strip().lower()
|
| 65 |
+
if failure_policy in {"exit", "warn"}:
|
| 66 |
+
return failure_policy
|
| 67 |
+
print(
|
| 68 |
+
f"Invalid HYPERVIEW_WARMUP_FAILURE_POLICY={failure_policy!r}; "
|
| 69 |
+
f"falling back to {DEFAULT_FAILURE_POLICY!r}."
|
| 70 |
+
)
|
| 71 |
+
return DEFAULT_FAILURE_POLICY
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _resolve_batch_insert_size() -> int:
|
| 75 |
+
raw_value = os.environ.get("HYPERVIEW_BATCH_INSERT_SIZE", str(DEFAULT_BATCH_INSERT_SIZE)).strip()
|
| 76 |
+
try:
|
| 77 |
+
batch_size = int(raw_value)
|
| 78 |
+
except ValueError as exc:
|
| 79 |
+
raise ValueError(f"Invalid integer value for HYPERVIEW_BATCH_INSERT_SIZE: {raw_value}") from exc
|
| 80 |
+
if batch_size <= 0:
|
| 81 |
+
raise ValueError(f"HYPERVIEW_BATCH_INSERT_SIZE must be > 0; got {batch_size}")
|
| 82 |
+
return batch_size
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _resolve_warmup_status_path() -> Path:
|
| 86 |
+
raw = os.environ.get("HYPERVIEW_WARMUP_STATUS_PATH")
|
| 87 |
+
if raw is None:
|
| 88 |
+
return DEFAULT_WARMUP_STATUS_PATH
|
| 89 |
+
return Path(raw)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class WarmupStatusTracker:
|
| 93 |
+
"""Tracks warmup state and persists it to a JSON status file."""
|
| 94 |
+
|
| 95 |
+
def __init__(self, status_path: Path):
|
| 96 |
+
self._status_path = status_path
|
| 97 |
+
self._lock = threading.Lock()
|
| 98 |
+
now = _utc_now()
|
| 99 |
+
self._state: dict[str, Any] = {
|
| 100 |
+
"status": "starting",
|
| 101 |
+
"phase": "boot",
|
| 102 |
+
"counts": {},
|
| 103 |
+
"error": None,
|
| 104 |
+
"timestamps": {
|
| 105 |
+
"started_at": now,
|
| 106 |
+
"updated_at": now,
|
| 107 |
+
},
|
| 108 |
+
}
|
| 109 |
+
self._persist_locked()
|
| 110 |
+
|
| 111 |
+
def update(
|
| 112 |
+
self,
|
| 113 |
+
*,
|
| 114 |
+
status: str | None = None,
|
| 115 |
+
phase: str | None = None,
|
| 116 |
+
counts: dict[str, Any] | None = None,
|
| 117 |
+
error: dict[str, Any] | None = None,
|
| 118 |
+
) -> None:
|
| 119 |
+
with self._lock:
|
| 120 |
+
now = _utc_now()
|
| 121 |
+
if status is not None:
|
| 122 |
+
self._state["status"] = status
|
| 123 |
+
if phase is not None:
|
| 124 |
+
self._state["phase"] = phase
|
| 125 |
+
if counts:
|
| 126 |
+
self._state["counts"].update(counts)
|
| 127 |
+
if error is not None:
|
| 128 |
+
self._state["error"] = error
|
| 129 |
+
self._state["timestamps"]["updated_at"] = now
|
| 130 |
+
if status == "ready":
|
| 131 |
+
self._state["timestamps"]["ready_at"] = now
|
| 132 |
+
if status == "failed":
|
| 133 |
+
self._state["timestamps"]["failed_at"] = now
|
| 134 |
+
self._persist_locked()
|
| 135 |
+
|
| 136 |
+
@property
|
| 137 |
+
def path(self) -> Path:
|
| 138 |
+
return self._status_path
|
| 139 |
+
|
| 140 |
+
def _persist_locked(self) -> None:
|
| 141 |
+
try:
|
| 142 |
+
self._status_path.parent.mkdir(parents=True, exist_ok=True)
|
| 143 |
+
tmp_path = self._status_path.with_name(f"{self._status_path.name}.tmp")
|
| 144 |
+
tmp_path.write_text(json.dumps(self._state, indent=2, sort_keys=True), encoding="utf-8")
|
| 145 |
+
tmp_path.replace(self._status_path)
|
| 146 |
+
except OSError as exc:
|
| 147 |
+
print(f"Warmup status warning: failed writing status JSON to {self._status_path} ({exc})")
|
| 148 |
+
|
| 149 |
|
| 150 |
def _patch_hyperview_default_panel() -> None:
|
| 151 |
"""Patch HyperView 0.3.1 frontend for default panel and dock cache-key migration.
|
|
|
|
| 281 |
return load_dataset(HF_DATASET_REPO, name=HF_DATASET_CONFIG, split=HF_DATASET_SPLIT)
|
| 282 |
|
| 283 |
|
| 284 |
+
def ingest_hf_dataset_samples(dataset: hv.Dataset, batch_insert_size: int | None = None) -> dict[str, int]:
|
| 285 |
rows = _load_hf_rows()
|
| 286 |
media_root = Path(os.environ.get("HYPERVIEW_MEDIA_DIR", "./demo_data/media")) / DATASET_NAME
|
| 287 |
media_root.mkdir(parents=True, exist_ok=True)
|
| 288 |
|
| 289 |
+
effective_batch_size = _resolve_batch_insert_size() if batch_insert_size is None else int(batch_insert_size)
|
| 290 |
+
if effective_batch_size <= 0:
|
| 291 |
+
raise ValueError(f"batch_insert_size must be > 0; got {effective_batch_size}")
|
| 292 |
+
|
| 293 |
+
records_by_id: dict[str, dict[str, Any]] = {}
|
| 294 |
+
duplicate_ids = 0
|
| 295 |
for index, row in enumerate(rows):
|
| 296 |
filename = str(row.get("filename", f"sample_{index:06d}.jpg"))
|
| 297 |
sample_id = str(row.get("sample_id", filename))
|
| 298 |
+
if sample_id in records_by_id:
|
| 299 |
+
duplicate_ids += 1
|
| 300 |
continue
|
| 301 |
+
records_by_id[sample_id] = {
|
| 302 |
+
"index": index,
|
| 303 |
+
"filename": filename,
|
| 304 |
+
"sample_id": sample_id,
|
| 305 |
+
"label": str(row.get("label", "")),
|
| 306 |
+
"split_tag": str(row.get("split_tag", "unknown")),
|
| 307 |
+
}
|
| 308 |
|
| 309 |
+
candidate_records = list(records_by_id.values())
|
| 310 |
+
candidate_ids = [record["sample_id"] for record in candidate_records]
|
| 311 |
+
existing_ids = dataset._storage.get_existing_ids(candidate_ids) if candidate_ids else set()
|
| 312 |
+
missing_records = [record for record in candidate_records if record["sample_id"] not in existing_ids]
|
| 313 |
+
|
| 314 |
+
print(
|
| 315 |
+
"HF ingestion plan: "
|
| 316 |
+
f"candidates={len(candidate_records)} existing={len(existing_ids)} "
|
| 317 |
+
f"missing={len(missing_records)} duplicates={duplicate_ids} "
|
| 318 |
+
f"batch_insert_size={effective_batch_size}"
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
added = 0
|
| 322 |
+
saved_images = 0
|
| 323 |
+
pending_samples: list[Sample] = []
|
| 324 |
+
|
| 325 |
+
def flush_pending_samples() -> None:
|
| 326 |
+
nonlocal added
|
| 327 |
+
if not pending_samples:
|
| 328 |
+
return
|
| 329 |
+
dataset._storage.add_samples_batch(pending_samples)
|
| 330 |
+
added += len(pending_samples)
|
| 331 |
+
print(f"Inserted sample batch: size={len(pending_samples)} total_inserted={added}")
|
| 332 |
+
pending_samples.clear()
|
| 333 |
+
|
| 334 |
+
for record in missing_records:
|
| 335 |
+
sample_id = str(record["sample_id"])
|
| 336 |
+
filename = str(record["filename"])
|
| 337 |
+
|
| 338 |
+
row = rows[int(record["index"])]
|
| 339 |
image_obj = row["image"]
|
| 340 |
image_path = media_root / f"{Path(sample_id).stem}.jpg"
|
| 341 |
if not image_path.exists():
|
| 342 |
image_obj.convert("RGB").save(image_path, format="JPEG", quality=90, optimize=True)
|
| 343 |
+
saved_images += 1
|
| 344 |
|
|
|
|
| 345 |
metadata = {
|
| 346 |
"filename": filename,
|
| 347 |
"sample_id": sample_id,
|
| 348 |
+
"split_tag": str(record["split_tag"]),
|
| 349 |
+
"identity": str(record["label"]),
|
| 350 |
"source_repo": HF_DATASET_REPO,
|
| 351 |
"source_config": HF_DATASET_CONFIG,
|
| 352 |
"source_split": HF_DATASET_SPLIT,
|
| 353 |
}
|
| 354 |
|
| 355 |
+
pending_samples.append(
|
| 356 |
Sample(
|
| 357 |
id=sample_id,
|
| 358 |
filepath=str(image_path),
|
| 359 |
+
label=str(record["label"]),
|
| 360 |
metadata=metadata,
|
| 361 |
)
|
| 362 |
)
|
|
|
|
| 363 |
|
| 364 |
+
if len(pending_samples) >= effective_batch_size:
|
| 365 |
+
flush_pending_samples()
|
| 366 |
+
|
| 367 |
+
flush_pending_samples()
|
| 368 |
+
|
| 369 |
+
print(
|
| 370 |
+
f"Ingested {added} HF samples into HyperView dataset '{DATASET_NAME}' "
|
| 371 |
+
f"(saved_images={saved_images}, existing={len(existing_ids)})."
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
return {
|
| 375 |
+
"candidates": len(candidate_records),
|
| 376 |
+
"existing": len(existing_ids),
|
| 377 |
+
"added": added,
|
| 378 |
+
"saved_images": saved_images,
|
| 379 |
+
"duplicates": duplicate_ids,
|
| 380 |
+
}
|
| 381 |
|
| 382 |
|
| 383 |
def ensure_embedding_spaces(dataset: hv.Dataset, asset_manifest: dict[str, Any], asset_dir: Path) -> None:
|
|
|
|
| 450 |
return layout_keys
|
| 451 |
|
| 452 |
|
| 453 |
+
def _run_warmup(dataset: hv.Dataset, tracker: WarmupStatusTracker) -> None:
|
| 454 |
asset_manifest = load_asset_manifest(ASSET_MANIFEST_PATH)
|
| 455 |
+
tracker.update(
|
| 456 |
+
status="running",
|
| 457 |
+
phase="ingest",
|
| 458 |
+
counts={"manifest_models": len(asset_manifest.get("models", []))},
|
| 459 |
+
)
|
| 460 |
|
| 461 |
+
batch_insert_size = _resolve_batch_insert_size()
|
| 462 |
if len(dataset) == 0:
|
| 463 |
print(
|
| 464 |
f"Loading HF dataset rows from {HF_DATASET_REPO}[{HF_DATASET_CONFIG}] split={HF_DATASET_SPLIT}"
|
| 465 |
)
|
| 466 |
+
ingest_stats = ingest_hf_dataset_samples(dataset, batch_insert_size=batch_insert_size)
|
| 467 |
+
else:
|
| 468 |
+
ingest_stats = {
|
| 469 |
+
"candidates": len(dataset),
|
| 470 |
+
"existing": len(dataset),
|
| 471 |
+
"added": 0,
|
| 472 |
+
"saved_images": 0,
|
| 473 |
+
"duplicates": 0,
|
| 474 |
+
}
|
| 475 |
+
print(f"Skipping HF ingestion because dataset '{DATASET_NAME}' already has {len(dataset)} samples.")
|
| 476 |
|
| 477 |
+
tracker.update(
|
| 478 |
+
counts={
|
| 479 |
+
"batch_insert_size": batch_insert_size,
|
| 480 |
+
"dataset_samples": len(dataset),
|
| 481 |
+
**ingest_stats,
|
| 482 |
+
}
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
tracker.update(phase="spaces")
|
| 486 |
ensure_embedding_spaces(dataset, asset_manifest=asset_manifest, asset_dir=EMBEDDING_ASSET_DIR)
|
| 487 |
+
tracker.update(counts={"spaces": len(dataset.list_spaces())})
|
| 488 |
+
|
| 489 |
+
tracker.update(phase="layouts")
|
| 490 |
layout_keys = ensure_layouts(dataset, asset_manifest=asset_manifest)
|
| 491 |
|
| 492 |
+
tracker.update(
|
| 493 |
+
status="ready",
|
| 494 |
+
phase="ready",
|
| 495 |
+
counts={
|
| 496 |
+
"dataset_samples": len(dataset),
|
| 497 |
+
"spaces": len(dataset.list_spaces()),
|
| 498 |
+
"layouts": len(layout_keys),
|
| 499 |
+
},
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
print(f"Dataset '{DATASET_NAME}' has {len(dataset)} samples")
|
| 503 |
print(f"Spaces: {[space.space_key for space in dataset.list_spaces()]}")
|
| 504 |
print(f"Layouts: {layout_keys}")
|
| 505 |
|
| 506 |
+
|
| 507 |
+
def _run_warmup_blocking(dataset: hv.Dataset, tracker: WarmupStatusTracker) -> None:
|
| 508 |
+
try:
|
| 509 |
+
_run_warmup(dataset, tracker)
|
| 510 |
+
except Exception as exc:
|
| 511 |
+
tb = traceback.format_exc()
|
| 512 |
+
tracker.update(
|
| 513 |
+
status="failed",
|
| 514 |
+
phase="failed",
|
| 515 |
+
error={
|
| 516 |
+
"type": type(exc).__name__,
|
| 517 |
+
"message": str(exc),
|
| 518 |
+
"traceback": tb,
|
| 519 |
+
},
|
| 520 |
+
)
|
| 521 |
+
print(tb)
|
| 522 |
+
raise
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
def _warmup_worker(
|
| 526 |
+
dataset: hv.Dataset,
|
| 527 |
+
tracker: WarmupStatusTracker,
|
| 528 |
+
failure_policy: str,
|
| 529 |
+
failure_event: threading.Event,
|
| 530 |
+
failure_holder: dict[str, str],
|
| 531 |
+
) -> None:
|
| 532 |
+
try:
|
| 533 |
+
_run_warmup(dataset, tracker)
|
| 534 |
+
except Exception as exc:
|
| 535 |
+
tb = traceback.format_exc()
|
| 536 |
+
tracker.update(
|
| 537 |
+
status="failed",
|
| 538 |
+
phase="failed",
|
| 539 |
+
error={
|
| 540 |
+
"type": type(exc).__name__,
|
| 541 |
+
"message": str(exc),
|
| 542 |
+
"traceback": tb,
|
| 543 |
+
},
|
| 544 |
+
)
|
| 545 |
+
print("Warmup failed:")
|
| 546 |
+
print(tb)
|
| 547 |
+
failure_holder["error"] = f"{type(exc).__name__}: {exc}"
|
| 548 |
+
if failure_policy == "exit":
|
| 549 |
+
failure_event.set()
|
| 550 |
+
|
| 551 |
+
|
| 552 |
+
def _start_server_session(dataset: hv.Dataset, bind_host: str, bind_port: int) -> Session:
|
| 553 |
+
session = Session(dataset, host=bind_host, port=bind_port)
|
| 554 |
+
session.start(background=True)
|
| 555 |
+
print(f"HyperView server is running at {session.url}")
|
| 556 |
+
return session
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
def _serve_forever(
|
| 560 |
+
session: Session,
|
| 561 |
+
*,
|
| 562 |
+
failure_event: threading.Event | None = None,
|
| 563 |
+
failure_holder: dict[str, str] | None = None,
|
| 564 |
+
) -> None:
|
| 565 |
+
try:
|
| 566 |
+
while True:
|
| 567 |
+
time.sleep(0.25)
|
| 568 |
+
if session._server_thread is not None and not session._server_thread.is_alive():
|
| 569 |
+
raise RuntimeError("HyperView server stopped unexpectedly.")
|
| 570 |
+
|
| 571 |
+
if failure_event is not None and failure_event.is_set():
|
| 572 |
+
reason = None
|
| 573 |
+
if failure_holder is not None:
|
| 574 |
+
reason = failure_holder.get("error")
|
| 575 |
+
if reason:
|
| 576 |
+
raise RuntimeError(f"Warmup failed and failure policy is 'exit': {reason}")
|
| 577 |
+
raise RuntimeError("Warmup failed and failure policy is 'exit'.")
|
| 578 |
+
except KeyboardInterrupt:
|
| 579 |
+
pass
|
| 580 |
+
finally:
|
| 581 |
+
session.stop()
|
| 582 |
+
if session._server_thread is not None:
|
| 583 |
+
session._server_thread.join(timeout=2.0)
|
| 584 |
|
| 585 |
|
| 586 |
def main() -> None:
|
| 587 |
_patch_hyperview_default_panel()
|
|
|
|
| 588 |
|
| 589 |
+
startup_mode = _resolve_startup_mode()
|
| 590 |
+
failure_policy = _resolve_failure_policy()
|
| 591 |
+
warmup_status_path = _resolve_warmup_status_path()
|
| 592 |
+
|
| 593 |
+
dataset = hv.Dataset(DATASET_NAME)
|
| 594 |
+
tracker = WarmupStatusTracker(warmup_status_path)
|
| 595 |
+
tracker.update(
|
| 596 |
+
counts={
|
| 597 |
+
"dataset_samples": len(dataset),
|
| 598 |
+
"startup_mode": startup_mode,
|
| 599 |
+
"failure_policy": failure_policy,
|
| 600 |
+
"batch_insert_size": _resolve_batch_insert_size(),
|
| 601 |
+
}
|
| 602 |
+
)
|
| 603 |
|
| 604 |
bind_host, bind_warning = _resolve_bind_host()
|
| 605 |
bind_port = _resolve_port()
|
| 606 |
|
| 607 |
if bind_warning:
|
| 608 |
print(f"Bind host notice: {bind_warning}")
|
| 609 |
+
|
| 610 |
print(
|
| 611 |
+
"Starting HyperView runtime with "
|
| 612 |
+
f"startup_mode={startup_mode} failure_policy={failure_policy} "
|
| 613 |
+
f"status_path={warmup_status_path} bind_host={bind_host} bind_port={bind_port} "
|
| 614 |
f"(SPACE_HOST={SPACE_HOST!r}, SPACE_PORT={os.environ.get('SPACE_PORT')!r}, "
|
| 615 |
f"PORT={os.environ.get('PORT')!r})"
|
| 616 |
)
|
| 617 |
+
|
| 618 |
+
if os.environ.get("HYPERVIEW_DEMO_PREP_ONLY") == "1":
|
| 619 |
+
_run_warmup_blocking(dataset, tracker)
|
| 620 |
+
print("Preparation-only mode enabled; skipping server launch.")
|
| 621 |
+
return
|
| 622 |
+
|
| 623 |
+
if startup_mode == "blocking":
|
| 624 |
+
_run_warmup_blocking(dataset, tracker)
|
| 625 |
+
session = _start_server_session(dataset, bind_host=bind_host, bind_port=bind_port)
|
| 626 |
+
_serve_forever(session)
|
| 627 |
+
return
|
| 628 |
+
|
| 629 |
+
failure_event = threading.Event()
|
| 630 |
+
failure_holder: dict[str, str] = {}
|
| 631 |
+
|
| 632 |
+
warmup_thread = threading.Thread(
|
| 633 |
+
target=_warmup_worker,
|
| 634 |
+
name="hyperview-warmup",
|
| 635 |
+
args=(dataset, tracker, failure_policy, failure_event, failure_holder),
|
| 636 |
+
daemon=True,
|
| 637 |
+
)
|
| 638 |
+
warmup_thread.start()
|
| 639 |
+
print("Warmup thread started in background.")
|
| 640 |
+
|
| 641 |
+
session = _start_server_session(dataset, bind_host=bind_host, bind_port=bind_port)
|
| 642 |
+
|
| 643 |
+
if failure_policy == "exit":
|
| 644 |
+
_serve_forever(session, failure_event=failure_event, failure_holder=failure_holder)
|
| 645 |
+
else:
|
| 646 |
+
_serve_forever(session)
|
| 647 |
|
| 648 |
|
| 649 |
if __name__ == "__main__":
|