Spaces:
Running
Running
Sync GitHub main af692537c27175c8410bf57a747be0caf23718e2 (part 7)
Browse files- scripts/build_frozen_environment.py +136 -0
- scripts/check_canonical_imports.py +48 -0
- scripts/check_cuda_kernel_freeze.py +56 -0
- scripts/check_cuda_parity_contract.py +109 -0
- scripts/check_cuda_semantic_boundary.py +86 -0
- scripts/run_cuda_hardware_acceptance.py +291 -0
- scripts/run_fe_acceptance.py +121 -0
- scripts/run_playground_cuda_smoke.py +119 -0
- scripts/validate_playground_ptx.py +44 -0
- src/acceleration/__init__.py +1 -0
- src/acceleration/cuda/__init__.py +34 -0
- src/acceleration/cuda/backend.py +176 -0
- src/acceleration/cuda/driver.py +478 -0
- src/acceleration/cuda/errors.py +9 -0
- src/acceleration/cuda/memory.py +13 -0
- src/acceleration/cuda/nvrtc.py +116 -0
- src/acceleration/cuda/plasticity/__init__.py +18 -0
- src/acceleration/cuda/plasticity/backend.py +268 -0
- src/acceleration/cuda/plasticity/contracts.py +28 -0
- src/acceleration/cuda/plasticity/kernel.cu +62 -0
- src/acceleration/cuda/plasticity/reference.py +161 -0
- src/acceleration/cuda/preflight.py +78 -0
- src/acceleration/cuda/ptxas.py +119 -0
- src/acceleration/cuda/recurrent/__init__.py +27 -0
- src/acceleration/cuda/recurrent/backend.py +375 -0
- src/acceleration/cuda/recurrent/config.py +116 -0
- src/acceleration/cuda/recurrent/kernel.cu +113 -0
- src/acceleration/cuda/recurrent/reference.py +153 -0
- src/acceleration/cuda/recurrent/state.py +223 -0
- src/controller/runtime.py +1 -1
- src/dashboard/__init__.py +6 -5
- src/dashboard/experiment_evaluation.py +166 -0
- src/dashboard/gate_status.py +1 -1
- src/dashboard/integration_status.py +201 -0
- src/dashboard/network_settings.py +69 -0
- src/dashboard/playground_api.py +375 -6
- src/dashboard/server.py +75 -2
- src/dashboard/static/experiment-workflow-base.js +128 -1
- src/dashboard/static/frontend/index.js +2 -0
- src/dashboard/static/frontend/modules/acceleration-integration.js +204 -0
- src/dashboard/static/frontend/modules/neuron-model-science.js +16 -16
- src/dashboard/static/frontend/modules/playground-workspace.js +859 -0
- src/dashboard/static/frontend/modules/playground.js +308 -37
- src/dashboard/static/frontend/modules/recurrent-snn-stage.js +17 -20
scripts/build_frozen_environment.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Build a deterministic Frozen-Environment engineering manifest artifact."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
|
| 9 |
+
from src.embodiment.deterministic import DeterministicTargetEnvironment
|
| 10 |
+
from src.embodiment.msba import SymbolFrame
|
| 11 |
+
from src.embodiment.neural_io_contracts import BoundaryFrame
|
| 12 |
+
from src.experience.frozen_environment import (
|
| 13 |
+
FreezeMode,
|
| 14 |
+
FrozenBoundaryFrame,
|
| 15 |
+
FrozenEnvironmentManifest,
|
| 16 |
+
RNGContractMode,
|
| 17 |
+
WorldRNGContract,
|
| 18 |
+
)
|
| 19 |
+
from src.verification.frozen_environment import write_manifest_artifact
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _boundary_frame(tick: int) -> FrozenBoundaryFrame:
|
| 23 |
+
symbol = SymbolFrame(
|
| 24 |
+
payload=json.dumps({"tick": tick}, sort_keys=True).encode("utf-8"),
|
| 25 |
+
codec="json",
|
| 26 |
+
sequence=tick,
|
| 27 |
+
provenance="fe-builder",
|
| 28 |
+
)
|
| 29 |
+
return FrozenBoundaryFrame.from_frame(
|
| 30 |
+
BoundaryFrame(
|
| 31 |
+
schema_version=1,
|
| 32 |
+
frame_id=f"fe-frame-{tick}",
|
| 33 |
+
stream_id="fe-sensor",
|
| 34 |
+
sequence=tick,
|
| 35 |
+
direction="INBOUND",
|
| 36 |
+
kind="DIGITAL",
|
| 37 |
+
content_type="application/json",
|
| 38 |
+
schema_id="fe-builder-v1",
|
| 39 |
+
symbol_frame=symbol,
|
| 40 |
+
correlation_id=f"fe-{tick}",
|
| 41 |
+
priority=0,
|
| 42 |
+
source_id="fe-builder",
|
| 43 |
+
provenance="engineering-verification",
|
| 44 |
+
arrival_time_ns=None,
|
| 45 |
+
admitted_tick=tick,
|
| 46 |
+
)
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _build(
|
| 51 |
+
mode: FreezeMode, *, target: int, ticks: int, seed: int
|
| 52 |
+
) -> FrozenEnvironmentManifest:
|
| 53 |
+
if ticks < 1:
|
| 54 |
+
raise ValueError("ticks must be >= 1")
|
| 55 |
+
schedule = tuple(range(ticks))
|
| 56 |
+
if mode is FreezeMode.FE1_BOUNDARY_REPLAY:
|
| 57 |
+
return FrozenEnvironmentManifest(
|
| 58 |
+
mode=mode,
|
| 59 |
+
environment_id="boundary-replay-v1",
|
| 60 |
+
environment_version="1",
|
| 61 |
+
environment_config={"builder": "deterministic-fixture"},
|
| 62 |
+
rng=WorldRNGContract(
|
| 63 |
+
algorithm="none",
|
| 64 |
+
version="1",
|
| 65 |
+
mode=RNGContractMode.NONE,
|
| 66 |
+
initial_fingerprint="none",
|
| 67 |
+
),
|
| 68 |
+
sensor_schedule=schedule,
|
| 69 |
+
action_schema={"kind": "none"},
|
| 70 |
+
reward_contract={"kind": "none"},
|
| 71 |
+
episode_policy={"reset": "explicit"},
|
| 72 |
+
boundary_frames=tuple(_boundary_frame(tick) for tick in schedule),
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
env = DeterministicTargetEnvironment(target=target)
|
| 76 |
+
env.reset(seed=seed)
|
| 77 |
+
return FrozenEnvironmentManifest(
|
| 78 |
+
mode=mode,
|
| 79 |
+
environment_id=env.environment_id,
|
| 80 |
+
environment_version="1",
|
| 81 |
+
environment_config={"target": target},
|
| 82 |
+
rng=WorldRNGContract(
|
| 83 |
+
algorithm="none",
|
| 84 |
+
version="1",
|
| 85 |
+
mode=RNGContractMode.NONE,
|
| 86 |
+
initial_fingerprint=env.rng_fingerprint(),
|
| 87 |
+
),
|
| 88 |
+
sensor_schedule=schedule,
|
| 89 |
+
action_schema={
|
| 90 |
+
"actuator_id": "target-actuator",
|
| 91 |
+
"actions": ["left", "right"],
|
| 92 |
+
},
|
| 93 |
+
reward_contract={"type": "target-hit", "hit_reward": 1.0},
|
| 94 |
+
episode_policy={
|
| 95 |
+
"reset": "explicit",
|
| 96 |
+
"pending_rewards_cross_episode": False,
|
| 97 |
+
},
|
| 98 |
+
initial_world_state=env.snapshot_state(),
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def main() -> int:
|
| 103 |
+
parser = argparse.ArgumentParser()
|
| 104 |
+
parser.add_argument(
|
| 105 |
+
"--mode", choices=[mode.value for mode in FreezeMode], required=True
|
| 106 |
+
)
|
| 107 |
+
parser.add_argument("--output", required=True)
|
| 108 |
+
parser.add_argument("--target", type=int, default=2)
|
| 109 |
+
parser.add_argument("--ticks", type=int, default=2)
|
| 110 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 111 |
+
args = parser.parse_args()
|
| 112 |
+
|
| 113 |
+
manifest = _build(
|
| 114 |
+
FreezeMode(args.mode),
|
| 115 |
+
target=args.target,
|
| 116 |
+
ticks=args.ticks,
|
| 117 |
+
seed=args.seed,
|
| 118 |
+
)
|
| 119 |
+
path = write_manifest_artifact(args.output, manifest)
|
| 120 |
+
print(
|
| 121 |
+
json.dumps(
|
| 122 |
+
{
|
| 123 |
+
"classification": "FROZEN_ENVIRONMENT_ENGINEERING_ARTIFACT",
|
| 124 |
+
"scientific_evidence": False,
|
| 125 |
+
"path": str(path),
|
| 126 |
+
"mode": manifest.mode.value,
|
| 127 |
+
"manifest_sha256": manifest.manifest_sha256,
|
| 128 |
+
},
|
| 129 |
+
sort_keys=True,
|
| 130 |
+
)
|
| 131 |
+
)
|
| 132 |
+
return 0
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
if __name__ == "__main__":
|
| 136 |
+
raise SystemExit(main())
|
scripts/check_canonical_imports.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fail-closed canonical import boundary gate for Wave 4."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import ast
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 9 |
+
TARGETS = (
|
| 10 |
+
ROOT / "src" / "acceleration",
|
| 11 |
+
ROOT / "src" / "verification",
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def forbidden_imports() -> list[str]:
|
| 16 |
+
failures: list[str] = []
|
| 17 |
+
for root in TARGETS:
|
| 18 |
+
for path in root.rglob("*.py"):
|
| 19 |
+
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
| 20 |
+
for node in ast.walk(tree):
|
| 21 |
+
if isinstance(node, ast.Import):
|
| 22 |
+
for alias in node.names:
|
| 23 |
+
if alias.name.startswith("src.playground"):
|
| 24 |
+
failures.append(
|
| 25 |
+
f"{path.relative_to(ROOT)}: import {alias.name}"
|
| 26 |
+
)
|
| 27 |
+
elif isinstance(node, ast.ImportFrom):
|
| 28 |
+
module = node.module or ""
|
| 29 |
+
if module.startswith("src.playground"):
|
| 30 |
+
failures.append(
|
| 31 |
+
f"{path.relative_to(ROOT)}: from {module} import ..."
|
| 32 |
+
)
|
| 33 |
+
return failures
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def main() -> int:
|
| 37 |
+
failures = forbidden_imports()
|
| 38 |
+
if failures:
|
| 39 |
+
raise SystemExit(
|
| 40 |
+
"canonical acceleration/verification layers import Playground:\n"
|
| 41 |
+
+ "\n".join(failures)
|
| 42 |
+
)
|
| 43 |
+
print("canonical import boundary: PASS")
|
| 44 |
+
return 0
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
if __name__ == "__main__":
|
| 48 |
+
raise SystemExit(main())
|
scripts/check_cuda_kernel_freeze.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Verify Wave-4 CUDA kernel extraction preserved kernel bytes exactly."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 9 |
+
PAIRS = (
|
| 10 |
+
(
|
| 11 |
+
ROOT / "src" / "playground" / "cuda" / "recurrent.cu",
|
| 12 |
+
ROOT / "src" / "acceleration" / "cuda" / "recurrent" / "kernel.cu",
|
| 13 |
+
),
|
| 14 |
+
(
|
| 15 |
+
ROOT / "src" / "playground" / "cuda" / "builder_synapses.cu",
|
| 16 |
+
ROOT / "src" / "acceleration" / "cuda" / "plasticity" / "kernel.cu",
|
| 17 |
+
),
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def sha256(path: Path) -> str:
|
| 22 |
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def kernel_mismatches() -> list[str]:
|
| 26 |
+
mismatches: list[str] = []
|
| 27 |
+
for legacy, canonical in PAIRS:
|
| 28 |
+
if not legacy.is_file() or not canonical.is_file():
|
| 29 |
+
mismatches.append(
|
| 30 |
+
f"missing kernel pair: {legacy.relative_to(ROOT)} / "
|
| 31 |
+
f"{canonical.relative_to(ROOT)}"
|
| 32 |
+
)
|
| 33 |
+
continue
|
| 34 |
+
if legacy.read_bytes() != canonical.read_bytes():
|
| 35 |
+
mismatches.append(
|
| 36 |
+
f"kernel changed during extraction: "
|
| 37 |
+
f"{legacy.relative_to(ROOT)}={sha256(legacy)} "
|
| 38 |
+
f"{canonical.relative_to(ROOT)}={sha256(canonical)}"
|
| 39 |
+
)
|
| 40 |
+
return mismatches
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def main() -> int:
|
| 44 |
+
mismatches = kernel_mismatches()
|
| 45 |
+
if mismatches:
|
| 46 |
+
raise SystemExit("\n".join(mismatches))
|
| 47 |
+
for legacy, canonical in PAIRS:
|
| 48 |
+
print(
|
| 49 |
+
f"kernel freeze PASS: {canonical.relative_to(ROOT)} "
|
| 50 |
+
f"sha256={sha256(canonical)}"
|
| 51 |
+
)
|
| 52 |
+
return 0
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
if __name__ == "__main__":
|
| 56 |
+
raise SystemExit(main())
|
scripts/check_cuda_parity_contract.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fail-closed Wave-4 canonical cross-backend parity contract gate.
|
| 2 |
+
|
| 3 |
+
Hosted CI does not have physical CUDA. This gate verifies that CPU and CUDA
|
| 4 |
+
conform to the canonical ExecutionBackend surface, share one canonical
|
| 5 |
+
configuration identity, and that the named physical acceptance test is wired
|
| 6 |
+
through canonical parity functions rather than Playground helpers.
|
| 7 |
+
|
| 8 |
+
Physical D1/D2 execution is performed by scripts/run_cuda_hardware_acceptance.py.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import ast
|
| 14 |
+
from collections.abc import Mapping
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
from src.acceleration.cuda import CUDABackend
|
| 18 |
+
from src.acceleration.cuda.recurrent import (
|
| 19 |
+
CPUReferenceBackend,
|
| 20 |
+
recurrent_fixture,
|
| 21 |
+
recurrent_inputs_to_mapping,
|
| 22 |
+
)
|
| 23 |
+
from src.runtime.backend import ExecutionBackend
|
| 24 |
+
from src.verification.parity import config_fingerprint
|
| 25 |
+
|
| 26 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 27 |
+
TEST_FILE = ROOT / "tests" / "test_cuda_wave4_backend.py"
|
| 28 |
+
HARDWARE_TEST = "test_physical_cpu_cuda_cross_backend_d1_d2_parity"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _hardware_test_source() -> str:
|
| 32 |
+
source = TEST_FILE.read_text(encoding="utf-8")
|
| 33 |
+
tree = ast.parse(source, filename=str(TEST_FILE))
|
| 34 |
+
for node in tree.body:
|
| 35 |
+
if (
|
| 36 |
+
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
| 37 |
+
and node.name == HARDWARE_TEST
|
| 38 |
+
):
|
| 39 |
+
segment = ast.get_source_segment(source, node)
|
| 40 |
+
if segment is None:
|
| 41 |
+
raise RuntimeError("could not recover hardware acceptance test source")
|
| 42 |
+
return segment
|
| 43 |
+
raise RuntimeError(f"missing named hardware acceptance test: {HARDWARE_TEST}")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def failures() -> list[str]:
|
| 47 |
+
problems: list[str] = []
|
| 48 |
+
|
| 49 |
+
cpu: ExecutionBackend = CPUReferenceBackend()
|
| 50 |
+
cuda: ExecutionBackend = CUDABackend()
|
| 51 |
+
config = recurrent_inputs_to_mapping(
|
| 52 |
+
recurrent_fixture(n_neurons=8, ticks=4, model="lif")
|
| 53 |
+
)
|
| 54 |
+
seed = 12345
|
| 55 |
+
cpu.initialize(config, seed)
|
| 56 |
+
cuda.initialize(config, seed)
|
| 57 |
+
|
| 58 |
+
cpu_config = cpu.snapshot().payload.get("config")
|
| 59 |
+
cuda_config = cuda.snapshot().payload.get("config")
|
| 60 |
+
if not isinstance(cpu_config, Mapping) or not isinstance(cuda_config, Mapping):
|
| 61 |
+
problems.append("backend snapshots must expose canonical mapping configs")
|
| 62 |
+
elif config_fingerprint(cpu_config) != config_fingerprint(cuda_config):
|
| 63 |
+
problems.append("CPU/CUDA canonical configuration fingerprints differ")
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
test_source = _hardware_test_source()
|
| 67 |
+
except RuntimeError as exc:
|
| 68 |
+
problems.append(str(exc))
|
| 69 |
+
test_source = ""
|
| 70 |
+
|
| 71 |
+
required_tokens = (
|
| 72 |
+
"CPUReferenceBackend",
|
| 73 |
+
"CUDABackend",
|
| 74 |
+
"exact_spike_parity",
|
| 75 |
+
"state_vector_parity",
|
| 76 |
+
"tolerance=1.0e-4",
|
| 77 |
+
)
|
| 78 |
+
for token in required_tokens:
|
| 79 |
+
if token not in test_source:
|
| 80 |
+
problems.append(
|
| 81 |
+
"hardware parity acceptance test is missing canonical token: " + token
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
full_test_source = TEST_FILE.read_text(encoding="utf-8")
|
| 85 |
+
if "MHRN_TEST_CUDA_HARDWARE" not in full_test_source:
|
| 86 |
+
problems.append("hardware parity acceptance test must remain opt-in")
|
| 87 |
+
|
| 88 |
+
if "playground" in test_source.lower():
|
| 89 |
+
problems.append(
|
| 90 |
+
"hardware parity acceptance test must not use Playground parity helpers"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
return problems
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def main() -> int:
|
| 97 |
+
problems = failures()
|
| 98 |
+
if problems:
|
| 99 |
+
raise SystemExit(
|
| 100 |
+
"Wave-4 canonical parity contract failed:\n" + "\n".join(problems)
|
| 101 |
+
)
|
| 102 |
+
print("Wave-4 canonical parity contract: PASS")
|
| 103 |
+
print("hardware acceptance test: PRESENT / OPT-IN")
|
| 104 |
+
print("D3 status: NOT_CLAIMED_IN_WAVE4")
|
| 105 |
+
return 0
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
if __name__ == "__main__":
|
| 109 |
+
raise SystemExit(main())
|
scripts/check_cuda_semantic_boundary.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fail-closed Wave-4 CUDA capability/learning-semantics gate."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from src.acceleration.cuda import CUDABackend
|
| 8 |
+
from src.acceleration.cuda.plasticity.contracts import (
|
| 9 |
+
LEARNING_CONTRACT_ID,
|
| 10 |
+
LEARNING_CONTRACT_STATUS,
|
| 11 |
+
PLASTICITY_SEMANTICS,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 15 |
+
LEARNING_CONTRACT = (
|
| 16 |
+
ROOT / "docs" / "02-architecture" / "MHRN_LEARNING_SYNAPSE_CONTRACT.md"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def failures() -> list[str]:
|
| 21 |
+
problems: list[str] = []
|
| 22 |
+
caps = CUDABackend().capabilities()
|
| 23 |
+
|
| 24 |
+
expected = {
|
| 25 |
+
"supports_recurrent": True,
|
| 26 |
+
"supports_plasticity": True,
|
| 27 |
+
"supports_pan_hyperstate": False,
|
| 28 |
+
"supports_structural_plasticity": False,
|
| 29 |
+
"max_neurons": 4096,
|
| 30 |
+
"max_ticks": 2000,
|
| 31 |
+
"max_edges": 65536,
|
| 32 |
+
"deterministic": True,
|
| 33 |
+
"plasticity_semantics": "NON_CANONICAL_DRAFT",
|
| 34 |
+
"execution_mode": "BOUNDED_REPLAY_REFERENCE",
|
| 35 |
+
}
|
| 36 |
+
actual = caps.to_mapping()
|
| 37 |
+
for key, value in expected.items():
|
| 38 |
+
if actual.get(key) != value:
|
| 39 |
+
problems.append(
|
| 40 |
+
f"CUDA capability mismatch: {key}={actual.get(key)!r}, expected {value!r}"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
if PLASTICITY_SEMANTICS != "NON_CANONICAL_DRAFT":
|
| 44 |
+
problems.append(
|
| 45 |
+
"plasticity semantics must remain NON_CANONICAL_DRAFT until contract alignment"
|
| 46 |
+
)
|
| 47 |
+
if LEARNING_CONTRACT_ID != "mhrn-learning-synapse-v1":
|
| 48 |
+
problems.append(f"unexpected learning contract id: {LEARNING_CONTRACT_ID}")
|
| 49 |
+
if LEARNING_CONTRACT_STATUS != "ALIGNMENT_PENDING":
|
| 50 |
+
problems.append(
|
| 51 |
+
"learning contract status must remain ALIGNMENT_PENDING in Wave 4"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
if not LEARNING_CONTRACT.is_file():
|
| 55 |
+
problems.append("canonical learning/synapse contract document is missing")
|
| 56 |
+
else:
|
| 57 |
+
text = LEARNING_CONTRACT.read_text(encoding="utf-8")
|
| 58 |
+
required = (
|
| 59 |
+
"canonical design contract; backend implementation alignment pending",
|
| 60 |
+
"learning_contract_id = mhrn-learning-synapse-v1",
|
| 61 |
+
"CPU and CUDA must not implement two merely similar learning rules",
|
| 62 |
+
)
|
| 63 |
+
for phrase in required:
|
| 64 |
+
if phrase not in text:
|
| 65 |
+
problems.append(
|
| 66 |
+
"learning contract document no longer contains required boundary: "
|
| 67 |
+
+ phrase
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
return problems
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def main() -> int:
|
| 74 |
+
problems = failures()
|
| 75 |
+
if problems:
|
| 76 |
+
raise SystemExit(
|
| 77 |
+
"Wave-4 CUDA semantic boundary failed:\n" + "\n".join(problems)
|
| 78 |
+
)
|
| 79 |
+
print("Wave-4 CUDA semantic boundary: PASS")
|
| 80 |
+
print("plasticity_semantics=NON_CANONICAL_DRAFT")
|
| 81 |
+
print("learning_contract_status=ALIGNMENT_PENDING")
|
| 82 |
+
return 0
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
if __name__ == "__main__":
|
| 86 |
+
raise SystemExit(main())
|
scripts/run_cuda_hardware_acceptance.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run canonical CUDA hardware acceptance on a physical NVIDIA GPU.
|
| 2 |
+
|
| 3 |
+
The runner always performs Wave-4 D1/D2. Optional FE-3 adds both the historical
|
| 4 |
+
Builder D3c bridge and the canonical FrozenEnvironment CPU-vs-CUDA live-loop
|
| 5 |
+
acceptance. All outputs are engineering verification only.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import argparse
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import shutil
|
| 14 |
+
import subprocess
|
| 15 |
+
import sys
|
| 16 |
+
from datetime import datetime, timezone
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 20 |
+
DEFAULT_FE3_MANIFEST = (
|
| 21 |
+
ROOT
|
| 22 |
+
/ "research"
|
| 23 |
+
/ "verification"
|
| 24 |
+
/ "frozen_environment"
|
| 25 |
+
/ "FE3_DETERMINISTIC_TARGET_V1.json"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _gpu_identity() -> str:
|
| 30 |
+
executable = shutil.which("nvidia-smi")
|
| 31 |
+
if executable is None:
|
| 32 |
+
raise SystemExit(
|
| 33 |
+
"CUDA hardware acceptance requires nvidia-smi on a physical NVIDIA host"
|
| 34 |
+
)
|
| 35 |
+
completed = subprocess.run(
|
| 36 |
+
[
|
| 37 |
+
executable,
|
| 38 |
+
"--query-gpu=name,driver_version",
|
| 39 |
+
"--format=csv,noheader",
|
| 40 |
+
],
|
| 41 |
+
check=True,
|
| 42 |
+
capture_output=True,
|
| 43 |
+
text=True,
|
| 44 |
+
)
|
| 45 |
+
identity = completed.stdout.strip()
|
| 46 |
+
if not identity:
|
| 47 |
+
raise SystemExit("nvidia-smi returned no GPU identity")
|
| 48 |
+
return identity
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _last_json_object(stdout: str) -> dict[str, object] | None:
|
| 52 |
+
for line in reversed(stdout.splitlines()):
|
| 53 |
+
line = line.strip()
|
| 54 |
+
if not line.startswith("{"):
|
| 55 |
+
continue
|
| 56 |
+
try:
|
| 57 |
+
value = json.loads(line)
|
| 58 |
+
except json.JSONDecodeError:
|
| 59 |
+
continue
|
| 60 |
+
if isinstance(value, dict):
|
| 61 |
+
return value
|
| 62 |
+
return None
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _run_command_case(
|
| 66 |
+
*,
|
| 67 |
+
label: str,
|
| 68 |
+
command: list[str],
|
| 69 |
+
env: dict[str, str],
|
| 70 |
+
parse_report: bool = False,
|
| 71 |
+
) -> dict[str, object]:
|
| 72 |
+
print(f"[{label}] Running:", " ".join(command))
|
| 73 |
+
completed = subprocess.run(
|
| 74 |
+
command,
|
| 75 |
+
cwd=ROOT,
|
| 76 |
+
env=env,
|
| 77 |
+
check=False,
|
| 78 |
+
capture_output=True,
|
| 79 |
+
text=True,
|
| 80 |
+
)
|
| 81 |
+
if completed.stdout:
|
| 82 |
+
print(completed.stdout, end="")
|
| 83 |
+
if completed.stderr:
|
| 84 |
+
print(completed.stderr, end="", file=sys.stderr)
|
| 85 |
+
result: dict[str, object] = {
|
| 86 |
+
"label": label,
|
| 87 |
+
"passed": completed.returncode == 0,
|
| 88 |
+
"returncode": completed.returncode,
|
| 89 |
+
"command": command,
|
| 90 |
+
}
|
| 91 |
+
if parse_report:
|
| 92 |
+
result["report"] = _last_json_object(completed.stdout)
|
| 93 |
+
return result
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _run_pytest_case(
|
| 97 |
+
*,
|
| 98 |
+
label: str,
|
| 99 |
+
selectors: list[str],
|
| 100 |
+
env: dict[str, str],
|
| 101 |
+
) -> dict[str, object]:
|
| 102 |
+
return _run_command_case(
|
| 103 |
+
label=label,
|
| 104 |
+
command=[sys.executable, "-m", "pytest", "-v", "--tb=short", *selectors],
|
| 105 |
+
env=env,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _write_artifacts(
|
| 110 |
+
report: dict[str, object],
|
| 111 |
+
output_dir: Path,
|
| 112 |
+
) -> tuple[Path, Path]:
|
| 113 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 114 |
+
day = datetime.now(timezone.utc).date().isoformat()
|
| 115 |
+
json_path = output_dir / f"HARDWARE_ACCEPTANCE_{day}.json"
|
| 116 |
+
markdown_path = output_dir / f"HARDWARE_ACCEPTANCE_{day}.md"
|
| 117 |
+
json_path.write_text(
|
| 118 |
+
json.dumps(report, sort_keys=True, indent=2, ensure_ascii=True) + "\n",
|
| 119 |
+
encoding="utf-8",
|
| 120 |
+
)
|
| 121 |
+
markdown_path.write_text(
|
| 122 |
+
"\n".join(
|
| 123 |
+
[
|
| 124 |
+
f"# Hardware Acceptance {day}",
|
| 125 |
+
"",
|
| 126 |
+
"**Classification:** Engineering Verification; not DATA/EVID.",
|
| 127 |
+
"",
|
| 128 |
+
f"- GPU: {report['gpu_identity']}",
|
| 129 |
+
f"- Wave-4 D1/D2: {report['wave4_physical']}",
|
| 130 |
+
f"- Builder D3c bridge: {report['builder_d3c_bridge']}",
|
| 131 |
+
f"- Canonical Frozen-Environment FE-3: {report['frozen_environment_fe3']}",
|
| 132 |
+
f"- Full FE-3 accepted: {report['full_fe3_accepted']}",
|
| 133 |
+
"",
|
| 134 |
+
"## Interpretation boundary",
|
| 135 |
+
"",
|
| 136 |
+
"- No scientific EVID or CLAIM is created.",
|
| 137 |
+
"- No speedup claim is created.",
|
| 138 |
+
"- PAN hyperstate is not in scope.",
|
| 139 |
+
"- Canonical learning semantics are not in scope.",
|
| 140 |
+
"- CPU/CUDA execution fingerprints are expected to differ because backend identity is provenance.",
|
| 141 |
+
"- Exact FE-3 acceptance requires the same manifest hash, live-input fingerprint and D3c trajectory hash.",
|
| 142 |
+
"",
|
| 143 |
+
]
|
| 144 |
+
),
|
| 145 |
+
encoding="utf-8",
|
| 146 |
+
)
|
| 147 |
+
return json_path, markdown_path
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def main() -> int:
|
| 151 |
+
parser = argparse.ArgumentParser()
|
| 152 |
+
parser.add_argument(
|
| 153 |
+
"--full",
|
| 154 |
+
action="store_true",
|
| 155 |
+
help="rerun all recurrent/plasticity physical acceptance cases too",
|
| 156 |
+
)
|
| 157 |
+
parser.add_argument(
|
| 158 |
+
"--include-fe3",
|
| 159 |
+
action="store_true",
|
| 160 |
+
help=(
|
| 161 |
+
"add canonical Frozen-Environment FE-3 CPU-vs-CUDA D3c acceptance "
|
| 162 |
+
"and the historical Builder D3c bridge; Wave-4 checks still run"
|
| 163 |
+
),
|
| 164 |
+
)
|
| 165 |
+
parser.add_argument(
|
| 166 |
+
"--manifest",
|
| 167 |
+
default=str(DEFAULT_FE3_MANIFEST),
|
| 168 |
+
help="versioned FE-3 manifest artifact used by --include-fe3",
|
| 169 |
+
)
|
| 170 |
+
parser.add_argument(
|
| 171 |
+
"--output-dir",
|
| 172 |
+
default=str(ROOT / "docs" / "canonical"),
|
| 173 |
+
help="write dated JSON and Markdown engineering-verification artifacts here",
|
| 174 |
+
)
|
| 175 |
+
parser.add_argument(
|
| 176 |
+
"--require-gpu",
|
| 177 |
+
default="",
|
| 178 |
+
help="fail unless the reported GPU identity contains this string",
|
| 179 |
+
)
|
| 180 |
+
args = parser.parse_args()
|
| 181 |
+
|
| 182 |
+
identity = _gpu_identity()
|
| 183 |
+
print(f"CUDA hardware identity: {identity}")
|
| 184 |
+
if args.require_gpu and args.require_gpu.lower() not in identity.lower():
|
| 185 |
+
raise SystemExit(
|
| 186 |
+
f"required GPU substring {args.require_gpu!r} not present in {identity!r}"
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
env = dict(os.environ)
|
| 190 |
+
env["MHRN_TEST_CUDA_HARDWARE"] = "1"
|
| 191 |
+
|
| 192 |
+
results: list[dict[str, object]] = []
|
| 193 |
+
results.append(
|
| 194 |
+
_run_pytest_case(
|
| 195 |
+
label="WAVE4_D1_D2",
|
| 196 |
+
selectors=[
|
| 197 |
+
"tests/test_cuda_wave4_backend.py::"
|
| 198 |
+
"test_physical_cpu_cuda_cross_backend_d1_d2_parity"
|
| 199 |
+
],
|
| 200 |
+
env=env,
|
| 201 |
+
)
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
if args.full:
|
| 205 |
+
results.append(
|
| 206 |
+
_run_pytest_case(
|
| 207 |
+
label="WAVE4_RECURRENT_PLASTICITY_FULL",
|
| 208 |
+
selectors=[
|
| 209 |
+
"tests/test_playground_cuda_recurrent.py",
|
| 210 |
+
"tests/test_playground_cuda_plasticity.py",
|
| 211 |
+
"-k",
|
| 212 |
+
"physical",
|
| 213 |
+
],
|
| 214 |
+
env=env,
|
| 215 |
+
)
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
bridge_status = "NOT_REQUESTED"
|
| 219 |
+
fe3_status = "NOT_REQUESTED"
|
| 220 |
+
full_fe3_accepted = False
|
| 221 |
+
if args.include_fe3:
|
| 222 |
+
bridge = _run_pytest_case(
|
| 223 |
+
label="BUILDER_D3C_BRIDGE",
|
| 224 |
+
selectors=[
|
| 225 |
+
"tests/test_playground_cuda_builder.py::"
|
| 226 |
+
"test_full_pan_builder_hardware_d3"
|
| 227 |
+
],
|
| 228 |
+
env=env,
|
| 229 |
+
)
|
| 230 |
+
results.append(bridge)
|
| 231 |
+
bridge_status = "PASS" if bool(bridge["passed"]) else "FAIL"
|
| 232 |
+
|
| 233 |
+
fe3 = _run_command_case(
|
| 234 |
+
label="FROZEN_ENVIRONMENT_FE3_CPU_CUDA_D3C",
|
| 235 |
+
command=[
|
| 236 |
+
sys.executable,
|
| 237 |
+
"scripts/run_fe_acceptance.py",
|
| 238 |
+
"--manifest",
|
| 239 |
+
str(Path(args.manifest)),
|
| 240 |
+
"--require-cuda",
|
| 241 |
+
],
|
| 242 |
+
env=env,
|
| 243 |
+
parse_report=True,
|
| 244 |
+
)
|
| 245 |
+
results.append(fe3)
|
| 246 |
+
fe3_status = "PASS" if bool(fe3["passed"]) else "FAIL"
|
| 247 |
+
full_fe3_accepted = bool(fe3["passed"])
|
| 248 |
+
|
| 249 |
+
passed = all(bool(item["passed"]) for item in results)
|
| 250 |
+
report: dict[str, object] = {
|
| 251 |
+
"classification": "CUDA_HARDWARE_ACCEPTANCE_REPORT",
|
| 252 |
+
"scientific_evidence": False,
|
| 253 |
+
"gpu_identity": identity,
|
| 254 |
+
"wave4_physical": "PASS" if bool(results[0]["passed"]) else "FAIL",
|
| 255 |
+
"builder_d3c_bridge": bridge_status,
|
| 256 |
+
"frozen_environment_fe3": fe3_status,
|
| 257 |
+
"full_fe3_accepted": full_fe3_accepted,
|
| 258 |
+
"fe3_manifest": str(Path(args.manifest)),
|
| 259 |
+
"kernel_semantics_changed": False,
|
| 260 |
+
"execution_fingerprint_rule": (
|
| 261 |
+
"BACKEND_IDENTITY_IS_PROVENANCE; CPU/CUDA FINGERPRINTS DIFFER"
|
| 262 |
+
),
|
| 263 |
+
"limitations": {
|
| 264 |
+
"speedup_claim": False,
|
| 265 |
+
"pan_hyperstate": False,
|
| 266 |
+
"canonical_learning_contract": False,
|
| 267 |
+
"scientific_data": False,
|
| 268 |
+
"evidence": False,
|
| 269 |
+
},
|
| 270 |
+
"results": results,
|
| 271 |
+
"passed": passed,
|
| 272 |
+
}
|
| 273 |
+
json_path, markdown_path = _write_artifacts(report, Path(args.output_dir))
|
| 274 |
+
report["artifact_json"] = str(json_path)
|
| 275 |
+
report["artifact_markdown"] = str(markdown_path)
|
| 276 |
+
print(json.dumps(report, sort_keys=True))
|
| 277 |
+
|
| 278 |
+
if not passed:
|
| 279 |
+
raise SystemExit(1)
|
| 280 |
+
|
| 281 |
+
print("CUDA hardware acceptance selections: PASS")
|
| 282 |
+
if args.include_fe3:
|
| 283 |
+
print(
|
| 284 |
+
"Canonical Frozen-Environment FE-3 CPU/CUDA D3c: PASS; "
|
| 285 |
+
"engineering verification only"
|
| 286 |
+
)
|
| 287 |
+
return 0
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
if __name__ == "__main__":
|
| 291 |
+
raise SystemExit(main())
|
scripts/run_fe_acceptance.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run FE-1/FE-2/FE-3 engineering acceptance for one frozen manifest."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
from collections.abc import Callable
|
| 10 |
+
|
| 11 |
+
from src.acceleration.cuda import CUDABackend
|
| 12 |
+
from src.acceleration.cuda.recurrent import CPUReferenceBackend
|
| 13 |
+
from src.embodiment.deterministic import DeterministicTargetEnvironment
|
| 14 |
+
from src.embodiment.models import ActionCommand, EnvironmentObservation
|
| 15 |
+
from src.experience.frozen_environment import FreezeMode, FrozenWorldAdapter
|
| 16 |
+
from src.verification.frozen_environment import (
|
| 17 |
+
load_manifest_artifact,
|
| 18 |
+
run_fe1_integrity,
|
| 19 |
+
run_fe2_replay_determinism,
|
| 20 |
+
run_fe3_backend_parity,
|
| 21 |
+
run_fe3_cpu_self_control,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _target_world_factory(target: int) -> Callable[[], FrozenWorldAdapter]:
|
| 26 |
+
def factory() -> FrozenWorldAdapter:
|
| 27 |
+
return DeterministicTargetEnvironment(target=target)
|
| 28 |
+
|
| 29 |
+
return factory
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _right_policy(
|
| 33 |
+
tick: int,
|
| 34 |
+
_previous: EnvironmentObservation | None,
|
| 35 |
+
) -> ActionCommand:
|
| 36 |
+
return ActionCommand("target-actuator", tick, "right")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def main() -> int:
|
| 40 |
+
parser = argparse.ArgumentParser()
|
| 41 |
+
parser.add_argument("--manifest", required=True)
|
| 42 |
+
parser.add_argument("--repeats", type=int, default=10)
|
| 43 |
+
parser.add_argument(
|
| 44 |
+
"--require-cuda",
|
| 45 |
+
action="store_true",
|
| 46 |
+
help=(
|
| 47 |
+
"Fail unless canonical FrozenEnvironment FE-3 has a live backend adapter; "
|
| 48 |
+
"the Builder D3c hardware bridge does not satisfy this gate."
|
| 49 |
+
),
|
| 50 |
+
)
|
| 51 |
+
args = parser.parse_args()
|
| 52 |
+
|
| 53 |
+
manifest = load_manifest_artifact(args.manifest)
|
| 54 |
+
results: list[dict[str, object]] = []
|
| 55 |
+
|
| 56 |
+
if manifest.mode is FreezeMode.FE1_BOUNDARY_REPLAY:
|
| 57 |
+
results.append(run_fe1_integrity(manifest).to_mapping())
|
| 58 |
+
else:
|
| 59 |
+
target = manifest.environment_config.get("target")
|
| 60 |
+
if type(target) is not int:
|
| 61 |
+
raise ValueError(
|
| 62 |
+
"acceptance CLI currently supports deterministic-target manifests only"
|
| 63 |
+
)
|
| 64 |
+
factory = _target_world_factory(target)
|
| 65 |
+
results.append(
|
| 66 |
+
run_fe2_replay_determinism(
|
| 67 |
+
manifest,
|
| 68 |
+
factory,
|
| 69 |
+
_right_policy,
|
| 70 |
+
repeats=args.repeats,
|
| 71 |
+
).to_mapping()
|
| 72 |
+
)
|
| 73 |
+
if manifest.mode is FreezeMode.FE3_FULL_DETERMINISTIC_LIVE_LOOP:
|
| 74 |
+
results.append(
|
| 75 |
+
run_fe3_cpu_self_control(
|
| 76 |
+
manifest,
|
| 77 |
+
factory,
|
| 78 |
+
_right_policy,
|
| 79 |
+
).to_mapping()
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
cuda_status = "NOT_REQUESTED"
|
| 83 |
+
passed = all(bool(item["passed"]) for item in results)
|
| 84 |
+
if args.require_cuda:
|
| 85 |
+
if manifest.mode is not FreezeMode.FE3_FULL_DETERMINISTIC_LIVE_LOOP:
|
| 86 |
+
cuda_status = "REQUIRES_FE3_MANIFEST"
|
| 87 |
+
passed = False
|
| 88 |
+
elif os.environ.get("MHRN_TEST_CUDA_HARDWARE") != "1":
|
| 89 |
+
cuda_status = "REQUIRES_MHRN_TEST_CUDA_HARDWARE"
|
| 90 |
+
passed = False
|
| 91 |
+
else:
|
| 92 |
+
target = manifest.environment_config.get("target")
|
| 93 |
+
if type(target) is not int:
|
| 94 |
+
raise ValueError(
|
| 95 |
+
"CUDA FE-3 acceptance supports deterministic-target manifests only"
|
| 96 |
+
)
|
| 97 |
+
hardware = run_fe3_backend_parity(
|
| 98 |
+
manifest,
|
| 99 |
+
_target_world_factory(target),
|
| 100 |
+
CPUReferenceBackend,
|
| 101 |
+
CUDABackend,
|
| 102 |
+
)
|
| 103 |
+
results.append(hardware.to_mapping())
|
| 104 |
+
cuda_status = "PASS" if hardware.passed else "FAIL"
|
| 105 |
+
passed = passed and hardware.passed
|
| 106 |
+
|
| 107 |
+
report = {
|
| 108 |
+
"classification": "FROZEN_ENVIRONMENT_ACCEPTANCE_REPORT",
|
| 109 |
+
"scientific_evidence": False,
|
| 110 |
+
"manifest_sha256": manifest.manifest_sha256,
|
| 111 |
+
"mode": manifest.mode.value,
|
| 112 |
+
"results": results,
|
| 113 |
+
"cuda_d3": cuda_status,
|
| 114 |
+
"passed": passed,
|
| 115 |
+
}
|
| 116 |
+
print(json.dumps(report, sort_keys=True))
|
| 117 |
+
return 0 if passed else 1
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
if __name__ == "__main__":
|
| 121 |
+
raise SystemExit(main())
|
scripts/run_playground_cuda_smoke.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run a real single-tick CUDA gate-kernel smoke/parity check.
|
| 2 |
+
|
| 3 |
+
This script requires:
|
| 4 |
+
- NVIDIA driver visible to the process;
|
| 5 |
+
- ptxas on PATH (CUDA toolkit);
|
| 6 |
+
- a supported NVIDIA GPU.
|
| 7 |
+
|
| 8 |
+
It is intentionally not part of ordinary hosted CI because GitHub-hosted Linux
|
| 9 |
+
runners do not expose an NVIDIA device.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
|
| 16 |
+
from src.playground.cuda import (
|
| 17 |
+
GateLaunchInputs,
|
| 18 |
+
compile_mapping,
|
| 19 |
+
cpu_gate_reference,
|
| 20 |
+
execute_gate_bundle,
|
| 21 |
+
gate_execution_parity_summary,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _inputs(bundle: object) -> GateLaunchInputs:
|
| 26 |
+
manifest = getattr(bundle, "manifest")
|
| 27 |
+
abi = manifest["kernel_abi"]
|
| 28 |
+
pan_dimensions = int(abi["pan_dimensions"])
|
| 29 |
+
return GateLaunchInputs.from_sequences(
|
| 30 |
+
input_current=[1.0, 2.0, 0.5, 1.5],
|
| 31 |
+
channel_masks=[0xFF] * 4,
|
| 32 |
+
amplitudes=[1.0] * 8,
|
| 33 |
+
reward_ring=[0.5],
|
| 34 |
+
action_map=[
|
| 35 |
+
0.0,
|
| 36 |
+
0.0,
|
| 37 |
+
0.0,
|
| 38 |
+
0.0,
|
| 39 |
+
0.0,
|
| 40 |
+
0.0,
|
| 41 |
+
0.0,
|
| 42 |
+
0.0,
|
| 43 |
+
3.0,
|
| 44 |
+
4.0,
|
| 45 |
+
5.0,
|
| 46 |
+
6.0,
|
| 47 |
+
0.0,
|
| 48 |
+
0.0,
|
| 49 |
+
0.0,
|
| 50 |
+
0.0,
|
| 51 |
+
0.0,
|
| 52 |
+
0.0,
|
| 53 |
+
0.0,
|
| 54 |
+
0.0,
|
| 55 |
+
0.0,
|
| 56 |
+
0.0,
|
| 57 |
+
0.0,
|
| 58 |
+
0.0,
|
| 59 |
+
0.0,
|
| 60 |
+
0.0,
|
| 61 |
+
0.0,
|
| 62 |
+
0.0,
|
| 63 |
+
0.0,
|
| 64 |
+
0.0,
|
| 65 |
+
0.0,
|
| 66 |
+
0.0,
|
| 67 |
+
],
|
| 68 |
+
feedback_matrix=[0.0] * (4 * pan_dimensions),
|
| 69 |
+
population=[0.0] * pan_dimensions,
|
| 70 |
+
logits=[
|
| 71 |
+
0.1,
|
| 72 |
+
0.2,
|
| 73 |
+
0.3,
|
| 74 |
+
0.9,
|
| 75 |
+
0.1,
|
| 76 |
+
0.8,
|
| 77 |
+
0.2,
|
| 78 |
+
0.3,
|
| 79 |
+
0.7,
|
| 80 |
+
0.2,
|
| 81 |
+
0.1,
|
| 82 |
+
0.0,
|
| 83 |
+
0.1,
|
| 84 |
+
0.2,
|
| 85 |
+
0.9,
|
| 86 |
+
0.3,
|
| 87 |
+
],
|
| 88 |
+
tick=0,
|
| 89 |
+
target_index=2,
|
| 90 |
+
previous_action=1,
|
| 91 |
+
seed=12345,
|
| 92 |
+
epsilon=0.0,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def main() -> int:
|
| 97 |
+
bundle = compile_mapping(
|
| 98 |
+
{
|
| 99 |
+
"closed_loop_preset": "minimal_closed_loop",
|
| 100 |
+
"pan_feedback_gain": 0.0,
|
| 101 |
+
}
|
| 102 |
+
)
|
| 103 |
+
inputs = _inputs(bundle)
|
| 104 |
+
reference = cpu_gate_reference(bundle, inputs)
|
| 105 |
+
cuda = execute_gate_bundle(bundle, inputs, block_size=64)
|
| 106 |
+
parity = gate_execution_parity_summary(reference, cuda)
|
| 107 |
+
result = {
|
| 108 |
+
"classification": "PLAYGROUND_CUDA1_3_HARDWARE_SMOKE",
|
| 109 |
+
"scientific_evidence": False,
|
| 110 |
+
"reference": reference,
|
| 111 |
+
"cuda": cuda,
|
| 112 |
+
"parity": parity,
|
| 113 |
+
}
|
| 114 |
+
print(json.dumps(result, indent=2, sort_keys=True))
|
| 115 |
+
return 0 if parity["passed"] else 1
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
if __name__ == "__main__":
|
| 119 |
+
raise SystemExit(main())
|
scripts/validate_playground_ptx.py
CHANGED
|
@@ -6,6 +6,8 @@ import json
|
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
from src.playground.cuda import assemble_bundle, compile_mapping
|
|
|
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
def main() -> int:
|
|
@@ -25,6 +27,48 @@ def main() -> int:
|
|
| 25 |
"spill_store_bytes": report.spill_store_bytes,
|
| 26 |
"spill_load_bytes": report.spill_load_bytes,
|
| 27 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
print(json.dumps(results, indent=2, sort_keys=True))
|
| 29 |
return 0
|
| 30 |
|
|
|
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
from src.playground.cuda import assemble_bundle, compile_mapping
|
| 9 |
+
from src.playground.cuda.nvrtc import compile_cuda_source
|
| 10 |
+
from src.playground.cuda.runtime import assemble_ptx
|
| 11 |
|
| 12 |
|
| 13 |
def main() -> int:
|
|
|
|
| 27 |
"spill_store_bytes": report.spill_store_bytes,
|
| 28 |
"spill_load_bytes": report.spill_load_bytes,
|
| 29 |
}
|
| 30 |
+
source = Path("src/playground/cuda/recurrent.cu").read_text()
|
| 31 |
+
recurrent_dir = root / "recurrent"
|
| 32 |
+
recurrent_dir.mkdir(parents=True, exist_ok=True)
|
| 33 |
+
ptx_path = recurrent_dir / "recurrent.ptx"
|
| 34 |
+
ptx_path.write_text(compile_cuda_source(source), encoding="utf-8")
|
| 35 |
+
recurrent_report = assemble_ptx(
|
| 36 |
+
ptx_path.read_text(), output_dir=recurrent_dir, target_sm="sm_86"
|
| 37 |
+
)
|
| 38 |
+
results["recurrent"] = recurrent_report.to_mapping()
|
| 39 |
+
plastic_report = assemble_ptx(
|
| 40 |
+
compile_cuda_source("#define PAN_PLASTIC\n" + source),
|
| 41 |
+
output_dir=root / "plasticity",
|
| 42 |
+
target_sm="sm_86",
|
| 43 |
+
)
|
| 44 |
+
results["plasticity"] = plastic_report.to_mapping()
|
| 45 |
+
results["builder_membrane"] = assemble_ptx(
|
| 46 |
+
compile_cuda_source(Path("src/playground/cuda/membrane.cu").read_text()),
|
| 47 |
+
output_dir=root / "builder-membrane",
|
| 48 |
+
target_sm="sm_86",
|
| 49 |
+
).to_mapping()
|
| 50 |
+
results["builder_pan_state"] = assemble_ptx(
|
| 51 |
+
compile_cuda_source(Path("src/playground/cuda/pan_state.cu").read_text()),
|
| 52 |
+
output_dir=root / "builder-pan-state",
|
| 53 |
+
target_sm="sm_86",
|
| 54 |
+
).to_mapping()
|
| 55 |
+
results["builder_synaptic_emission"] = assemble_ptx(
|
| 56 |
+
compile_cuda_source(
|
| 57 |
+
Path("src/playground/cuda/builder_synapses.cu").read_text()
|
| 58 |
+
),
|
| 59 |
+
output_dir=root / "builder-synaptic-emission",
|
| 60 |
+
target_sm="sm_86",
|
| 61 |
+
).to_mapping()
|
| 62 |
+
results["resident_delay_queue"] = assemble_ptx(
|
| 63 |
+
compile_cuda_source(Path("src/playground/cuda/delay_queue.cu").read_text()),
|
| 64 |
+
output_dir=root / "resident-delay-queue",
|
| 65 |
+
target_sm="sm_86",
|
| 66 |
+
).to_mapping()
|
| 67 |
+
results["resident_neuron_traces"] = assemble_ptx(
|
| 68 |
+
compile_cuda_source(Path("src/playground/cuda/neuron_traces.cu").read_text()),
|
| 69 |
+
output_dir=root / "resident-neuron-traces",
|
| 70 |
+
target_sm="sm_86",
|
| 71 |
+
).to_mapping()
|
| 72 |
print(json.dumps(results, indent=2, sort_keys=True))
|
| 73 |
return 0
|
| 74 |
|
src/acceleration/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Canonical hardware acceleration interfaces."""
|
src/acceleration/cuda/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical CUDA acceleration layer.
|
| 2 |
+
|
| 3 |
+
No module in this package may import :mod:`src.playground`.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from .backend import CUDABackend, execution_backend_contract_check
|
| 7 |
+
from .driver import CudaDriver, DriverModule
|
| 8 |
+
from .errors import CudaDriverError, CudaRuntimeUnavailable
|
| 9 |
+
from .memory import DeviceAllocation
|
| 10 |
+
from .nvrtc import compile_cuda_source, toolkit_root
|
| 11 |
+
from .preflight import (
|
| 12 |
+
CooperativePreflight,
|
| 13 |
+
cooperative_capacity,
|
| 14 |
+
validate_cuda_block_size,
|
| 15 |
+
)
|
| 16 |
+
from .ptxas import PtxasReport, assemble_ptx, parse_ptxas_verbose
|
| 17 |
+
|
| 18 |
+
__all__ = [
|
| 19 |
+
"CUDABackend",
|
| 20 |
+
"CooperativePreflight",
|
| 21 |
+
"CudaDriver",
|
| 22 |
+
"CudaDriverError",
|
| 23 |
+
"CudaRuntimeUnavailable",
|
| 24 |
+
"DeviceAllocation",
|
| 25 |
+
"DriverModule",
|
| 26 |
+
"PtxasReport",
|
| 27 |
+
"assemble_ptx",
|
| 28 |
+
"compile_cuda_source",
|
| 29 |
+
"cooperative_capacity",
|
| 30 |
+
"parse_ptxas_verbose",
|
| 31 |
+
"toolkit_root",
|
| 32 |
+
"execution_backend_contract_check",
|
| 33 |
+
"validate_cuda_block_size",
|
| 34 |
+
]
|
src/acceleration/cuda/backend.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ExecutionBackend facade for the bounded Wave-4 CUDA reference."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Mapping, Sequence
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from src.runtime.backend import (
|
| 9 |
+
BackendCapabilities,
|
| 10 |
+
BackendState,
|
| 11 |
+
ExecutionBackend,
|
| 12 |
+
RunResult,
|
| 13 |
+
StepResult,
|
| 14 |
+
)
|
| 15 |
+
from src.verification.parity import config_fingerprint, execution_fingerprint
|
| 16 |
+
|
| 17 |
+
from .plasticity.contracts import PLASTICITY_SEMANTICS
|
| 18 |
+
from .recurrent.backend import execute_recurrent
|
| 19 |
+
from .recurrent.state import (
|
| 20 |
+
canonical_int,
|
| 21 |
+
prefix_inputs,
|
| 22 |
+
recurrent_inputs_from_mapping,
|
| 23 |
+
recurrent_inputs_to_mapping,
|
| 24 |
+
set_external_tick_config,
|
| 25 |
+
stable_digest,
|
| 26 |
+
step_payload,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class CUDABackend:
|
| 31 |
+
"""Bounded replay adapter over the hardware-verified recurrent kernel."""
|
| 32 |
+
|
| 33 |
+
backend_name = "cuda"
|
| 34 |
+
backend_version = "wave4-bounded-replay-v1"
|
| 35 |
+
|
| 36 |
+
def __init__(self) -> None:
|
| 37 |
+
self._config: dict[str, object] | None = None
|
| 38 |
+
self._seed = 0
|
| 39 |
+
self._tick = 0
|
| 40 |
+
|
| 41 |
+
def initialize(self, config: Mapping[str, object], seed: int) -> None:
|
| 42 |
+
inputs = recurrent_inputs_from_mapping(config, seed=seed)
|
| 43 |
+
self._config = recurrent_inputs_to_mapping(inputs)
|
| 44 |
+
self._seed = seed
|
| 45 |
+
self._tick = 0
|
| 46 |
+
|
| 47 |
+
def _require_config(self) -> dict[str, object]:
|
| 48 |
+
if self._config is None:
|
| 49 |
+
raise RuntimeError("backend is not initialized")
|
| 50 |
+
return self._config
|
| 51 |
+
|
| 52 |
+
def _execute_prefix(self, total_ticks: int) -> Mapping[str, object]:
|
| 53 |
+
config = self._require_config()
|
| 54 |
+
inputs = recurrent_inputs_from_mapping(config, seed=self._seed)
|
| 55 |
+
result = execute_recurrent(prefix_inputs(inputs, total_ticks))
|
| 56 |
+
outputs = result.get("outputs")
|
| 57 |
+
if not isinstance(outputs, Mapping):
|
| 58 |
+
raise RuntimeError("CUDA recurrent execution returned no outputs")
|
| 59 |
+
return outputs
|
| 60 |
+
|
| 61 |
+
def _step_from_outputs(
|
| 62 |
+
self, outputs: Mapping[str, object], tick: int
|
| 63 |
+
) -> StepResult:
|
| 64 |
+
config = self._require_config()
|
| 65 |
+
payload = step_payload(
|
| 66 |
+
outputs,
|
| 67 |
+
tick=tick,
|
| 68 |
+
n_neurons=canonical_int(config["n_neurons"], field="n_neurons"),
|
| 69 |
+
)
|
| 70 |
+
spikes_raw = payload["spikes"]
|
| 71 |
+
if not isinstance(spikes_raw, list):
|
| 72 |
+
raise ValueError("spike payload must be a list")
|
| 73 |
+
spikes = tuple(
|
| 74 |
+
index for index, value in enumerate(spikes_raw) if int(value) == 1
|
| 75 |
+
)
|
| 76 |
+
return StepResult(
|
| 77 |
+
tick=tick + 1,
|
| 78 |
+
spikes=spikes,
|
| 79 |
+
state_digest=stable_digest(payload),
|
| 80 |
+
metrics={
|
| 81 |
+
"voltage": payload["voltage"],
|
| 82 |
+
"adaptation": payload["adaptation"],
|
| 83 |
+
"spike_count": len(spikes),
|
| 84 |
+
},
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
def set_external_tick(self, tick: int, currents: Sequence[float]) -> None:
|
| 88 |
+
"""Inject one live input row without changing CUDA kernel semantics."""
|
| 89 |
+
|
| 90 |
+
if tick != self._tick:
|
| 91 |
+
raise ValueError("live external input tick must equal continuation cursor")
|
| 92 |
+
set_external_tick_config(self._require_config(), tick=tick, currents=currents)
|
| 93 |
+
|
| 94 |
+
def step(self, tick: int) -> StepResult:
|
| 95 |
+
if tick != self._tick:
|
| 96 |
+
raise ValueError("tick must equal the backend continuation cursor")
|
| 97 |
+
config = self._require_config()
|
| 98 |
+
if tick >= canonical_int(config["ticks"], field="ticks"):
|
| 99 |
+
raise ValueError("configured tick limit reached")
|
| 100 |
+
outputs = self._execute_prefix(tick + 1)
|
| 101 |
+
self._tick = tick + 1
|
| 102 |
+
return self._step_from_outputs(outputs, tick)
|
| 103 |
+
|
| 104 |
+
def run(self, ticks: int) -> RunResult:
|
| 105 |
+
if type(ticks) is not int or ticks < 1:
|
| 106 |
+
raise ValueError("ticks must be a positive int")
|
| 107 |
+
config = self._require_config()
|
| 108 |
+
total = self._tick + ticks
|
| 109 |
+
if total > canonical_int(config["ticks"], field="ticks"):
|
| 110 |
+
raise ValueError("requested run exceeds configured tick limit")
|
| 111 |
+
start = self._tick
|
| 112 |
+
outputs = self._execute_prefix(total)
|
| 113 |
+
steps = tuple(
|
| 114 |
+
self._step_from_outputs(outputs, tick) for tick in range(start, total)
|
| 115 |
+
)
|
| 116 |
+
self._tick = total
|
| 117 |
+
return RunResult(
|
| 118 |
+
ticks_requested=ticks,
|
| 119 |
+
steps=steps,
|
| 120 |
+
final_state=self.snapshot(),
|
| 121 |
+
execution_fingerprint=execution_fingerprint(
|
| 122 |
+
seed=self._seed,
|
| 123 |
+
config_hash=config_fingerprint(config),
|
| 124 |
+
backend_name=self.backend_name,
|
| 125 |
+
backend_version=self.backend_version,
|
| 126 |
+
ticks=total,
|
| 127 |
+
),
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
def snapshot(self) -> BackendState:
|
| 131 |
+
config = self._require_config()
|
| 132 |
+
payload: dict[str, Any] = {
|
| 133 |
+
"backend": self.backend_version,
|
| 134 |
+
"seed": self._seed,
|
| 135 |
+
"tick": self._tick,
|
| 136 |
+
"config": config,
|
| 137 |
+
"replay_required": True,
|
| 138 |
+
}
|
| 139 |
+
return BackendState(
|
| 140 |
+
tick=self._tick,
|
| 141 |
+
payload=payload,
|
| 142 |
+
state_digest=stable_digest(payload),
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
def restore(self, state: BackendState) -> None:
|
| 146 |
+
payload = dict(state.payload)
|
| 147 |
+
if payload.get("backend") != self.backend_version:
|
| 148 |
+
raise ValueError("checkpoint backend version mismatch")
|
| 149 |
+
raw_config = payload.get("config")
|
| 150 |
+
raw_seed = payload.get("seed")
|
| 151 |
+
if not isinstance(raw_config, Mapping) or type(raw_seed) is not int:
|
| 152 |
+
raise ValueError("checkpoint is missing canonical config/seed")
|
| 153 |
+
self.initialize(raw_config, raw_seed)
|
| 154 |
+
if state.tick > canonical_int(self._require_config()["ticks"], field="ticks"):
|
| 155 |
+
raise ValueError("checkpoint tick exceeds configured limit")
|
| 156 |
+
self._tick = state.tick
|
| 157 |
+
|
| 158 |
+
def capabilities(self) -> BackendCapabilities:
|
| 159 |
+
return BackendCapabilities(
|
| 160 |
+
supports_recurrent=True,
|
| 161 |
+
supports_plasticity=True,
|
| 162 |
+
supports_pan_hyperstate=False,
|
| 163 |
+
supports_structural_plasticity=False,
|
| 164 |
+
max_neurons=4096,
|
| 165 |
+
max_ticks=2000,
|
| 166 |
+
max_edges=65536,
|
| 167 |
+
deterministic=True,
|
| 168 |
+
plasticity_semantics=PLASTICITY_SEMANTICS,
|
| 169 |
+
execution_mode="BOUNDED_REPLAY_REFERENCE",
|
| 170 |
+
supports_live_external_input=True,
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def execution_backend_contract_check() -> bool:
|
| 175 |
+
backend: ExecutionBackend = CUDABackend()
|
| 176 |
+
return isinstance(backend, ExecutionBackend)
|
src/acceleration/cuda/driver.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical CUDA Driver API wrapper.
|
| 2 |
+
|
| 3 |
+
This module owns device/context/module/memory/launch mechanics only. It has no
|
| 4 |
+
Playground, PAN, Gate-IR or scientific semantics.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import ctypes
|
| 10 |
+
import math
|
| 11 |
+
import os
|
| 12 |
+
from collections.abc import Sequence
|
| 13 |
+
from dataclasses import dataclass
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
from .errors import CudaDriverError, CudaRuntimeUnavailable
|
| 18 |
+
from .memory import DeviceAllocation
|
| 19 |
+
from .preflight import CooperativePreflight, validate_cuda_block_size
|
| 20 |
+
|
| 21 |
+
# CUDA Driver API attribute IDs from cuda.h.
|
| 22 |
+
_CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT = 16
|
| 23 |
+
_CU_DEVICE_ATTRIBUTE_COOPERATIVE_LAUNCH = 95
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass(frozen=True, slots=True)
|
| 27 |
+
class DriverModule:
|
| 28 |
+
"""Opaque handles returned by CUDA Driver API loading."""
|
| 29 |
+
|
| 30 |
+
context: ctypes.c_void_p
|
| 31 |
+
module: ctypes.c_void_p
|
| 32 |
+
function: ctypes.c_void_p
|
| 33 |
+
device_ordinal: int
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class CudaDriver:
|
| 37 |
+
"""Minimal ctypes wrapper for CUDA-1 loading, memory and kernel launch."""
|
| 38 |
+
|
| 39 |
+
def __init__(self, library: str | None = None) -> None:
|
| 40 |
+
candidates = (
|
| 41 |
+
[library]
|
| 42 |
+
if library
|
| 43 |
+
else (["nvcuda.dll"] if os.name == "nt" else ["libcuda.so.1", "libcuda.so"])
|
| 44 |
+
)
|
| 45 |
+
loader = (
|
| 46 |
+
getattr(ctypes, "WinDLL", ctypes.CDLL) if os.name == "nt" else ctypes.CDLL
|
| 47 |
+
)
|
| 48 |
+
last_error: OSError | None = None
|
| 49 |
+
for candidate in candidates:
|
| 50 |
+
try:
|
| 51 |
+
self._lib = loader(candidate)
|
| 52 |
+
break
|
| 53 |
+
except OSError as exc:
|
| 54 |
+
last_error = exc
|
| 55 |
+
else:
|
| 56 |
+
names = ", ".join(candidates)
|
| 57 |
+
raise CudaRuntimeUnavailable(
|
| 58 |
+
f"CUDA driver library not available; tried: {names}"
|
| 59 |
+
) from last_error
|
| 60 |
+
self._bind()
|
| 61 |
+
|
| 62 |
+
def _bind(self) -> None:
|
| 63 |
+
lib = self._lib
|
| 64 |
+
lib.cuInit.argtypes = [ctypes.c_uint]
|
| 65 |
+
lib.cuInit.restype = ctypes.c_int
|
| 66 |
+
lib.cuDeviceGet.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.c_int]
|
| 67 |
+
lib.cuDeviceGet.restype = ctypes.c_int
|
| 68 |
+
lib.cuDeviceGetAttribute.argtypes = [
|
| 69 |
+
ctypes.POINTER(ctypes.c_int),
|
| 70 |
+
ctypes.c_int,
|
| 71 |
+
ctypes.c_int,
|
| 72 |
+
]
|
| 73 |
+
lib.cuDeviceGetAttribute.restype = ctypes.c_int
|
| 74 |
+
lib.cuCtxCreate_v2.argtypes = [
|
| 75 |
+
ctypes.POINTER(ctypes.c_void_p),
|
| 76 |
+
ctypes.c_uint,
|
| 77 |
+
ctypes.c_int,
|
| 78 |
+
]
|
| 79 |
+
lib.cuCtxCreate_v2.restype = ctypes.c_int
|
| 80 |
+
lib.cuCtxDestroy_v2.argtypes = [ctypes.c_void_p]
|
| 81 |
+
lib.cuCtxDestroy_v2.restype = ctypes.c_int
|
| 82 |
+
lib.cuModuleLoad.argtypes = [
|
| 83 |
+
ctypes.POINTER(ctypes.c_void_p),
|
| 84 |
+
ctypes.c_char_p,
|
| 85 |
+
]
|
| 86 |
+
lib.cuModuleLoad.restype = ctypes.c_int
|
| 87 |
+
lib.cuModuleUnload.argtypes = [ctypes.c_void_p]
|
| 88 |
+
lib.cuModuleUnload.restype = ctypes.c_int
|
| 89 |
+
lib.cuModuleGetFunction.argtypes = [
|
| 90 |
+
ctypes.POINTER(ctypes.c_void_p),
|
| 91 |
+
ctypes.c_void_p,
|
| 92 |
+
ctypes.c_char_p,
|
| 93 |
+
]
|
| 94 |
+
lib.cuModuleGetFunction.restype = ctypes.c_int
|
| 95 |
+
lib.cuOccupancyMaxActiveBlocksPerMultiprocessor.argtypes = [
|
| 96 |
+
ctypes.POINTER(ctypes.c_int),
|
| 97 |
+
ctypes.c_void_p,
|
| 98 |
+
ctypes.c_int,
|
| 99 |
+
ctypes.c_size_t,
|
| 100 |
+
]
|
| 101 |
+
lib.cuOccupancyMaxActiveBlocksPerMultiprocessor.restype = ctypes.c_int
|
| 102 |
+
lib.cuMemAlloc_v2.argtypes = [
|
| 103 |
+
ctypes.POINTER(ctypes.c_uint64),
|
| 104 |
+
ctypes.c_size_t,
|
| 105 |
+
]
|
| 106 |
+
lib.cuMemAlloc_v2.restype = ctypes.c_int
|
| 107 |
+
lib.cuMemFree_v2.argtypes = [ctypes.c_uint64]
|
| 108 |
+
lib.cuMemFree_v2.restype = ctypes.c_int
|
| 109 |
+
lib.cuMemGetInfo_v2.argtypes = [
|
| 110 |
+
ctypes.POINTER(ctypes.c_size_t),
|
| 111 |
+
ctypes.POINTER(ctypes.c_size_t),
|
| 112 |
+
]
|
| 113 |
+
lib.cuMemGetInfo_v2.restype = ctypes.c_int
|
| 114 |
+
lib.cuMemcpyHtoD_v2.argtypes = [
|
| 115 |
+
ctypes.c_uint64,
|
| 116 |
+
ctypes.c_void_p,
|
| 117 |
+
ctypes.c_size_t,
|
| 118 |
+
]
|
| 119 |
+
lib.cuMemcpyHtoD_v2.restype = ctypes.c_int
|
| 120 |
+
lib.cuMemcpyDtoH_v2.argtypes = [
|
| 121 |
+
ctypes.c_void_p,
|
| 122 |
+
ctypes.c_uint64,
|
| 123 |
+
ctypes.c_size_t,
|
| 124 |
+
]
|
| 125 |
+
lib.cuMemcpyDtoH_v2.restype = ctypes.c_int
|
| 126 |
+
lib.cuLaunchKernel.argtypes = [
|
| 127 |
+
ctypes.c_void_p,
|
| 128 |
+
ctypes.c_uint,
|
| 129 |
+
ctypes.c_uint,
|
| 130 |
+
ctypes.c_uint,
|
| 131 |
+
ctypes.c_uint,
|
| 132 |
+
ctypes.c_uint,
|
| 133 |
+
ctypes.c_uint,
|
| 134 |
+
ctypes.c_uint,
|
| 135 |
+
ctypes.c_void_p,
|
| 136 |
+
ctypes.POINTER(ctypes.c_void_p),
|
| 137 |
+
ctypes.POINTER(ctypes.c_void_p),
|
| 138 |
+
]
|
| 139 |
+
lib.cuLaunchKernel.restype = ctypes.c_int
|
| 140 |
+
lib.cuCtxSynchronize.argtypes = []
|
| 141 |
+
lib.cuCtxSynchronize.restype = ctypes.c_int
|
| 142 |
+
|
| 143 |
+
@staticmethod
|
| 144 |
+
def _check(code: int, call: str) -> None:
|
| 145 |
+
if code != 0:
|
| 146 |
+
raise CudaDriverError(f"{call} failed with CUDA error code {code}")
|
| 147 |
+
|
| 148 |
+
def initialize(self) -> None:
|
| 149 |
+
self._check(self._lib.cuInit(0), "cuInit")
|
| 150 |
+
|
| 151 |
+
def _device(self, ordinal: int) -> int:
|
| 152 |
+
device = ctypes.c_int()
|
| 153 |
+
self._check(
|
| 154 |
+
self._lib.cuDeviceGet(ctypes.byref(device), ordinal),
|
| 155 |
+
"cuDeviceGet",
|
| 156 |
+
)
|
| 157 |
+
return int(device.value)
|
| 158 |
+
|
| 159 |
+
def device_attribute(self, ordinal: int, attribute: int) -> int:
|
| 160 |
+
device = self._device(ordinal)
|
| 161 |
+
value = ctypes.c_int()
|
| 162 |
+
self._check(
|
| 163 |
+
self._lib.cuDeviceGetAttribute(
|
| 164 |
+
ctypes.byref(value),
|
| 165 |
+
attribute,
|
| 166 |
+
device,
|
| 167 |
+
),
|
| 168 |
+
"cuDeviceGetAttribute",
|
| 169 |
+
)
|
| 170 |
+
return int(value.value)
|
| 171 |
+
|
| 172 |
+
def alloc_device(self, size_bytes: int) -> DeviceAllocation:
|
| 173 |
+
"""Allocate device memory owned by the current CUDA context."""
|
| 174 |
+
|
| 175 |
+
if size_bytes <= 0:
|
| 176 |
+
raise ValueError("size_bytes must be positive")
|
| 177 |
+
ptr = ctypes.c_uint64()
|
| 178 |
+
self._check(
|
| 179 |
+
self._lib.cuMemAlloc_v2(ctypes.byref(ptr), size_bytes),
|
| 180 |
+
"cuMemAlloc_v2",
|
| 181 |
+
)
|
| 182 |
+
return DeviceAllocation(ptr=int(ptr.value), size_bytes=size_bytes)
|
| 183 |
+
|
| 184 |
+
def free_device(self, allocation: DeviceAllocation) -> None:
|
| 185 |
+
"""Release one device allocation."""
|
| 186 |
+
|
| 187 |
+
if allocation.ptr:
|
| 188 |
+
self._check(
|
| 189 |
+
self._lib.cuMemFree_v2(ctypes.c_uint64(allocation.ptr)),
|
| 190 |
+
"cuMemFree_v2",
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
def memory_info(self) -> tuple[int, int]:
|
| 194 |
+
"""Return free and total bytes for the current CUDA context."""
|
| 195 |
+
|
| 196 |
+
free_bytes = ctypes.c_size_t()
|
| 197 |
+
total_bytes = ctypes.c_size_t()
|
| 198 |
+
self._check(
|
| 199 |
+
self._lib.cuMemGetInfo_v2(
|
| 200 |
+
ctypes.byref(free_bytes),
|
| 201 |
+
ctypes.byref(total_bytes),
|
| 202 |
+
),
|
| 203 |
+
"cuMemGetInfo_v2",
|
| 204 |
+
)
|
| 205 |
+
return int(free_bytes.value), int(total_bytes.value)
|
| 206 |
+
|
| 207 |
+
def copy_host_to_device(
|
| 208 |
+
self,
|
| 209 |
+
allocation: DeviceAllocation,
|
| 210 |
+
source: ctypes.Array[Any],
|
| 211 |
+
*,
|
| 212 |
+
size_bytes: int | None = None,
|
| 213 |
+
) -> None:
|
| 214 |
+
"""Copy a ctypes-backed host buffer to device memory."""
|
| 215 |
+
|
| 216 |
+
count = allocation.size_bytes if size_bytes is None else size_bytes
|
| 217 |
+
host_bytes = ctypes.sizeof(source)
|
| 218 |
+
if count < 0 or count > allocation.size_bytes:
|
| 219 |
+
raise ValueError("host-to-device copy exceeds device allocation")
|
| 220 |
+
if count > host_bytes:
|
| 221 |
+
raise ValueError(
|
| 222 |
+
f"host-to-device copy exceeds host buffer: {count} > {host_bytes}"
|
| 223 |
+
)
|
| 224 |
+
self._check(
|
| 225 |
+
self._lib.cuMemcpyHtoD_v2(
|
| 226 |
+
ctypes.c_uint64(allocation.ptr),
|
| 227 |
+
ctypes.cast(source, ctypes.c_void_p),
|
| 228 |
+
count,
|
| 229 |
+
),
|
| 230 |
+
"cuMemcpyHtoD_v2",
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
def copy_device_to_host(
|
| 234 |
+
self,
|
| 235 |
+
destination: ctypes.Array[Any],
|
| 236 |
+
allocation: DeviceAllocation,
|
| 237 |
+
*,
|
| 238 |
+
size_bytes: int | None = None,
|
| 239 |
+
) -> None:
|
| 240 |
+
"""Copy device memory into a ctypes-backed host buffer."""
|
| 241 |
+
|
| 242 |
+
count = allocation.size_bytes if size_bytes is None else size_bytes
|
| 243 |
+
host_bytes = ctypes.sizeof(destination)
|
| 244 |
+
if count < 0 or count > allocation.size_bytes:
|
| 245 |
+
raise ValueError("device-to-host copy exceeds device allocation")
|
| 246 |
+
if count > host_bytes:
|
| 247 |
+
raise ValueError(
|
| 248 |
+
f"device-to-host copy exceeds host buffer: {count} > {host_bytes}"
|
| 249 |
+
)
|
| 250 |
+
self._check(
|
| 251 |
+
self._lib.cuMemcpyDtoH_v2(
|
| 252 |
+
ctypes.cast(destination, ctypes.c_void_p),
|
| 253 |
+
ctypes.c_uint64(allocation.ptr),
|
| 254 |
+
count,
|
| 255 |
+
),
|
| 256 |
+
"cuMemcpyDtoH_v2",
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
def launch_kernel(
|
| 260 |
+
self,
|
| 261 |
+
loaded: DriverModule,
|
| 262 |
+
*,
|
| 263 |
+
grid: tuple[int, int, int],
|
| 264 |
+
block: tuple[int, int, int],
|
| 265 |
+
arguments: Sequence[object],
|
| 266 |
+
dynamic_shared_bytes: int = 0,
|
| 267 |
+
) -> None:
|
| 268 |
+
"""Launch a loaded kernel with CUDA Driver API argument packing."""
|
| 269 |
+
|
| 270 |
+
if min(*grid, *block) <= 0:
|
| 271 |
+
raise ValueError("grid and block dimensions must be positive")
|
| 272 |
+
storage = list(arguments)
|
| 273 |
+
kernel_params = (ctypes.c_void_p * len(storage))()
|
| 274 |
+
for index, argument in enumerate(storage):
|
| 275 |
+
kernel_params[index] = ctypes.cast(
|
| 276 |
+
ctypes.byref(argument), ctypes.c_void_p # type: ignore[arg-type]
|
| 277 |
+
)
|
| 278 |
+
self._check(
|
| 279 |
+
self._lib.cuLaunchKernel(
|
| 280 |
+
loaded.function,
|
| 281 |
+
grid[0],
|
| 282 |
+
grid[1],
|
| 283 |
+
grid[2],
|
| 284 |
+
block[0],
|
| 285 |
+
block[1],
|
| 286 |
+
block[2],
|
| 287 |
+
dynamic_shared_bytes,
|
| 288 |
+
None,
|
| 289 |
+
kernel_params,
|
| 290 |
+
None,
|
| 291 |
+
),
|
| 292 |
+
"cuLaunchKernel",
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
def synchronize(self) -> None:
|
| 296 |
+
"""Synchronize the current CUDA context."""
|
| 297 |
+
|
| 298 |
+
self._check(self._lib.cuCtxSynchronize(), "cuCtxSynchronize")
|
| 299 |
+
|
| 300 |
+
def launch_cooperative(
|
| 301 |
+
self,
|
| 302 |
+
loaded: DriverModule,
|
| 303 |
+
*,
|
| 304 |
+
n_neurons: int,
|
| 305 |
+
block_size: int,
|
| 306 |
+
arguments: Sequence[object],
|
| 307 |
+
) -> CooperativePreflight:
|
| 308 |
+
"""Launch a resident 1D grid only after checking actual kernel occupancy."""
|
| 309 |
+
preflight = self.cooperative_preflight(
|
| 310 |
+
loaded, n_neurons=n_neurons, block_size=block_size
|
| 311 |
+
)
|
| 312 |
+
if n_neurons < 1 or not preflight.launch_fits:
|
| 313 |
+
raise ValueError(
|
| 314 |
+
"cooperative grid exceeds the device/kernel resident capacity"
|
| 315 |
+
)
|
| 316 |
+
launch = self._lib.cuLaunchCooperativeKernel
|
| 317 |
+
launch.argtypes = [
|
| 318 |
+
ctypes.c_void_p,
|
| 319 |
+
ctypes.c_uint,
|
| 320 |
+
ctypes.c_uint,
|
| 321 |
+
ctypes.c_uint,
|
| 322 |
+
ctypes.c_uint,
|
| 323 |
+
ctypes.c_uint,
|
| 324 |
+
ctypes.c_uint,
|
| 325 |
+
ctypes.c_uint,
|
| 326 |
+
ctypes.c_void_p,
|
| 327 |
+
ctypes.POINTER(ctypes.c_void_p),
|
| 328 |
+
]
|
| 329 |
+
launch.restype = ctypes.c_int
|
| 330 |
+
storage = list(arguments)
|
| 331 |
+
parameters = (ctypes.c_void_p * len(storage))()
|
| 332 |
+
for index, argument in enumerate(storage):
|
| 333 |
+
parameters[index] = ctypes.cast(
|
| 334 |
+
ctypes.byref(argument), ctypes.c_void_p # type: ignore[arg-type]
|
| 335 |
+
)
|
| 336 |
+
self._check(
|
| 337 |
+
launch(
|
| 338 |
+
loaded.function,
|
| 339 |
+
preflight.required_blocks,
|
| 340 |
+
1,
|
| 341 |
+
1,
|
| 342 |
+
block_size,
|
| 343 |
+
1,
|
| 344 |
+
1,
|
| 345 |
+
0,
|
| 346 |
+
None,
|
| 347 |
+
parameters,
|
| 348 |
+
),
|
| 349 |
+
"cuLaunchCooperativeKernel",
|
| 350 |
+
)
|
| 351 |
+
return preflight
|
| 352 |
+
|
| 353 |
+
def load_cubin(
|
| 354 |
+
self,
|
| 355 |
+
cubin_path: Path,
|
| 356 |
+
*,
|
| 357 |
+
kernel_name: str = "pan_gate_kernel",
|
| 358 |
+
device_ordinal: int = 0,
|
| 359 |
+
) -> DriverModule:
|
| 360 |
+
"""Load a ptxas-generated cubin and resolve one kernel symbol."""
|
| 361 |
+
|
| 362 |
+
self.initialize()
|
| 363 |
+
device = self._device(device_ordinal)
|
| 364 |
+
context = ctypes.c_void_p()
|
| 365 |
+
self._check(
|
| 366 |
+
self._lib.cuCtxCreate_v2(ctypes.byref(context), 0, device),
|
| 367 |
+
"cuCtxCreate_v2",
|
| 368 |
+
)
|
| 369 |
+
module = ctypes.c_void_p()
|
| 370 |
+
try:
|
| 371 |
+
self._check(
|
| 372 |
+
self._lib.cuModuleLoad(
|
| 373 |
+
ctypes.byref(module),
|
| 374 |
+
str(cubin_path).encode("utf-8"),
|
| 375 |
+
),
|
| 376 |
+
"cuModuleLoad",
|
| 377 |
+
)
|
| 378 |
+
function = ctypes.c_void_p()
|
| 379 |
+
self._check(
|
| 380 |
+
self._lib.cuModuleGetFunction(
|
| 381 |
+
ctypes.byref(function),
|
| 382 |
+
module,
|
| 383 |
+
kernel_name.encode("ascii"),
|
| 384 |
+
),
|
| 385 |
+
"cuModuleGetFunction",
|
| 386 |
+
)
|
| 387 |
+
except Exception:
|
| 388 |
+
if module.value:
|
| 389 |
+
self._lib.cuModuleUnload(module)
|
| 390 |
+
self._lib.cuCtxDestroy_v2(context)
|
| 391 |
+
raise
|
| 392 |
+
return DriverModule(
|
| 393 |
+
context=context,
|
| 394 |
+
module=module,
|
| 395 |
+
function=function,
|
| 396 |
+
device_ordinal=device_ordinal,
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
def kernel(self, loaded: DriverModule, name: str) -> DriverModule:
|
| 400 |
+
"""Borrow another function in the same owned module/context; do not unload it separately."""
|
| 401 |
+
if not name or not name.isascii() or not name.replace("_", "").isalnum():
|
| 402 |
+
raise ValueError("invalid CUDA kernel symbol")
|
| 403 |
+
function = ctypes.c_void_p()
|
| 404 |
+
self._check(
|
| 405 |
+
self._lib.cuModuleGetFunction(
|
| 406 |
+
ctypes.byref(function), loaded.module, name.encode("ascii")
|
| 407 |
+
),
|
| 408 |
+
"cuModuleGetFunction",
|
| 409 |
+
)
|
| 410 |
+
return DriverModule(
|
| 411 |
+
loaded.context, loaded.module, function, loaded.device_ordinal
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
def unload(self, loaded: DriverModule) -> None:
|
| 415 |
+
module_error: CudaDriverError | None = None
|
| 416 |
+
if loaded.module.value:
|
| 417 |
+
try:
|
| 418 |
+
self._check(
|
| 419 |
+
self._lib.cuModuleUnload(loaded.module),
|
| 420 |
+
"cuModuleUnload",
|
| 421 |
+
)
|
| 422 |
+
except CudaDriverError as exc:
|
| 423 |
+
module_error = exc
|
| 424 |
+
if loaded.context.value:
|
| 425 |
+
try:
|
| 426 |
+
self._check(
|
| 427 |
+
self._lib.cuCtxDestroy_v2(loaded.context),
|
| 428 |
+
"cuCtxDestroy_v2",
|
| 429 |
+
)
|
| 430 |
+
except CudaDriverError:
|
| 431 |
+
if module_error is None:
|
| 432 |
+
raise
|
| 433 |
+
if module_error is not None:
|
| 434 |
+
raise module_error
|
| 435 |
+
|
| 436 |
+
def cooperative_preflight(
|
| 437 |
+
self,
|
| 438 |
+
loaded: DriverModule,
|
| 439 |
+
*,
|
| 440 |
+
n_neurons: int,
|
| 441 |
+
block_size: int = 128,
|
| 442 |
+
dynamic_shared_bytes: int = 0,
|
| 443 |
+
) -> CooperativePreflight:
|
| 444 |
+
"""Calculate whether the full cooperative grid can be resident."""
|
| 445 |
+
|
| 446 |
+
validate_cuda_block_size(block_size)
|
| 447 |
+
cooperative = bool(
|
| 448 |
+
self.device_attribute(
|
| 449 |
+
loaded.device_ordinal,
|
| 450 |
+
_CU_DEVICE_ATTRIBUTE_COOPERATIVE_LAUNCH,
|
| 451 |
+
)
|
| 452 |
+
)
|
| 453 |
+
sms = self.device_attribute(
|
| 454 |
+
loaded.device_ordinal,
|
| 455 |
+
_CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT,
|
| 456 |
+
)
|
| 457 |
+
active = ctypes.c_int()
|
| 458 |
+
self._check(
|
| 459 |
+
self._lib.cuOccupancyMaxActiveBlocksPerMultiprocessor(
|
| 460 |
+
ctypes.byref(active),
|
| 461 |
+
loaded.function,
|
| 462 |
+
block_size,
|
| 463 |
+
dynamic_shared_bytes,
|
| 464 |
+
),
|
| 465 |
+
"cuOccupancyMaxActiveBlocksPerMultiprocessor",
|
| 466 |
+
)
|
| 467 |
+
required = math.ceil(n_neurons / block_size)
|
| 468 |
+
capacity = int(active.value) * sms
|
| 469 |
+
return CooperativePreflight(
|
| 470 |
+
cooperative_launch=cooperative,
|
| 471 |
+
multiprocessor_count=sms,
|
| 472 |
+
active_blocks_per_sm=int(active.value),
|
| 473 |
+
block_size=block_size,
|
| 474 |
+
required_blocks=required,
|
| 475 |
+
resident_block_capacity=capacity,
|
| 476 |
+
launch_fits=cooperative and required <= capacity,
|
| 477 |
+
n_neurons=n_neurons,
|
| 478 |
+
)
|
src/acceleration/cuda/errors.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical CUDA acceleration errors."""
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class CudaRuntimeUnavailable(RuntimeError):
|
| 5 |
+
"""Raised when an optional CUDA-1 dependency is not available."""
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class CudaDriverError(RuntimeError):
|
| 9 |
+
"""Raised when a CUDA Driver API call fails."""
|
src/acceleration/cuda/memory.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical CUDA memory ownership contracts."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass(frozen=True, slots=True)
|
| 9 |
+
class DeviceAllocation:
|
| 10 |
+
"""One owned CUDA device allocation."""
|
| 11 |
+
|
| 12 |
+
ptr: int
|
| 13 |
+
size_bytes: int
|
src/acceleration/cuda/nvrtc.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Optional CUDA source compilation without a host C++ compiler."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import ctypes
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
import shutil
|
| 9 |
+
from contextlib import nullcontext
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
from .errors import CudaRuntimeUnavailable
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def toolkit_root() -> Path:
|
| 16 |
+
candidates = [os.environ.get("CUDA_PATH", ""), os.environ.get("CUDA_HOME", "")]
|
| 17 |
+
compiler = shutil.which("nvcc")
|
| 18 |
+
if compiler:
|
| 19 |
+
candidates.append(str(Path(compiler).resolve().parent.parent))
|
| 20 |
+
candidates.append("/usr/local/cuda")
|
| 21 |
+
if os.name == "nt":
|
| 22 |
+
base = Path(os.environ.get("ProgramFiles", "C:/Program Files"))
|
| 23 |
+
candidates.extend(
|
| 24 |
+
str(p)
|
| 25 |
+
for p in sorted(
|
| 26 |
+
(base / "NVIDIA GPU Computing Toolkit/CUDA").glob("v*"), reverse=True
|
| 27 |
+
)
|
| 28 |
+
)
|
| 29 |
+
for candidate in candidates:
|
| 30 |
+
if candidate and (Path(candidate) / "include/cooperative_groups.h").is_file():
|
| 31 |
+
return Path(candidate)
|
| 32 |
+
raise CudaRuntimeUnavailable("CUDA toolkit headers/NVRTC are required")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def compile_cuda_source(source: str, *, target_sm: str = "sm_86") -> str:
|
| 36 |
+
"""Compile a bounded trusted source string to PTX; no GPU is required."""
|
| 37 |
+
if not re.fullmatch(r"sm_[0-9]{2,3}", target_sm):
|
| 38 |
+
raise ValueError("invalid CUDA architecture")
|
| 39 |
+
root = toolkit_root()
|
| 40 |
+
patterns = (
|
| 41 |
+
["bin/nvrtc64_*.dll", "bin/x64/nvrtc64_*.dll"]
|
| 42 |
+
if os.name == "nt"
|
| 43 |
+
else ["lib64/libnvrtc.so*", "targets/x86_64-linux/lib/libnvrtc.so*"]
|
| 44 |
+
)
|
| 45 |
+
libraries = [
|
| 46 |
+
p for pattern in patterns for p in root.glob(pattern) if ".alt." not in p.name
|
| 47 |
+
]
|
| 48 |
+
if not libraries:
|
| 49 |
+
raise CudaRuntimeUnavailable("NVRTC library is not installed")
|
| 50 |
+
library = libraries[0]
|
| 51 |
+
directory = (
|
| 52 |
+
getattr(os, "add_dll_directory")(str(library.parent))
|
| 53 |
+
if os.name == "nt"
|
| 54 |
+
else nullcontext()
|
| 55 |
+
)
|
| 56 |
+
with directory:
|
| 57 |
+
lib = ctypes.CDLL(str(library))
|
| 58 |
+
lib.nvrtcCreateProgram.argtypes = [
|
| 59 |
+
ctypes.POINTER(ctypes.c_void_p),
|
| 60 |
+
ctypes.c_char_p,
|
| 61 |
+
ctypes.c_char_p,
|
| 62 |
+
ctypes.c_int,
|
| 63 |
+
ctypes.c_void_p,
|
| 64 |
+
ctypes.c_void_p,
|
| 65 |
+
]
|
| 66 |
+
lib.nvrtcCompileProgram.argtypes = [
|
| 67 |
+
ctypes.c_void_p,
|
| 68 |
+
ctypes.c_int,
|
| 69 |
+
ctypes.POINTER(ctypes.c_char_p),
|
| 70 |
+
]
|
| 71 |
+
lib.nvrtcGetProgramLogSize.argtypes = [
|
| 72 |
+
ctypes.c_void_p,
|
| 73 |
+
ctypes.POINTER(ctypes.c_size_t),
|
| 74 |
+
]
|
| 75 |
+
lib.nvrtcGetProgramLog.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
| 76 |
+
lib.nvrtcGetPTXSize.argtypes = [
|
| 77 |
+
ctypes.c_void_p,
|
| 78 |
+
ctypes.POINTER(ctypes.c_size_t),
|
| 79 |
+
]
|
| 80 |
+
lib.nvrtcGetPTX.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
| 81 |
+
lib.nvrtcDestroyProgram.argtypes = [ctypes.POINTER(ctypes.c_void_p)]
|
| 82 |
+
program = ctypes.c_void_p()
|
| 83 |
+
code = lib.nvrtcCreateProgram(
|
| 84 |
+
ctypes.byref(program), source.encode(), b"recurrent.cu", 0, None, None
|
| 85 |
+
)
|
| 86 |
+
if code:
|
| 87 |
+
raise CudaRuntimeUnavailable(f"nvrtcCreateProgram failed: {code}")
|
| 88 |
+
try:
|
| 89 |
+
options = [
|
| 90 |
+
f"--gpu-architecture=compute_{target_sm[3:]}",
|
| 91 |
+
f"--include-path={root / 'include'}",
|
| 92 |
+
"--std=c++17",
|
| 93 |
+
"--fmad=false",
|
| 94 |
+
]
|
| 95 |
+
if (root / "include/cccl").is_dir():
|
| 96 |
+
options.append(f"--include-path={root / 'include/cccl'}")
|
| 97 |
+
encoded = (ctypes.c_char_p * len(options))(
|
| 98 |
+
*(item.encode() for item in options)
|
| 99 |
+
)
|
| 100 |
+
code = lib.nvrtcCompileProgram(program, len(options), encoded)
|
| 101 |
+
size = ctypes.c_size_t()
|
| 102 |
+
lib.nvrtcGetProgramLogSize(program, ctypes.byref(size))
|
| 103 |
+
log = ctypes.create_string_buffer(max(1, size.value))
|
| 104 |
+
lib.nvrtcGetProgramLog(program, log)
|
| 105 |
+
if code:
|
| 106 |
+
raise CudaRuntimeUnavailable(
|
| 107 |
+
f"NVRTC compilation failed ({code}): {log.value.decode(errors='replace')}"
|
| 108 |
+
)
|
| 109 |
+
if lib.nvrtcGetPTXSize(program, ctypes.byref(size)):
|
| 110 |
+
raise CudaRuntimeUnavailable("NVRTC PTX size unavailable")
|
| 111 |
+
ptx = ctypes.create_string_buffer(size.value)
|
| 112 |
+
if lib.nvrtcGetPTX(program, ptx):
|
| 113 |
+
raise CudaRuntimeUnavailable("NVRTC PTX retrieval failed")
|
| 114 |
+
return ptx.value.decode()
|
| 115 |
+
finally:
|
| 116 |
+
lib.nvrtcDestroyProgram(ctypes.byref(program))
|
src/acceleration/cuda/plasticity/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical CUDA plasticity engineering package."""
|
| 2 |
+
|
| 3 |
+
from .backend import BuilderSynapseStepper
|
| 4 |
+
from .contracts import (
|
| 5 |
+
LEARNING_CONTRACT_ID,
|
| 6 |
+
LEARNING_CONTRACT_STATUS,
|
| 7 |
+
PLASTICITY_SEMANTICS,
|
| 8 |
+
)
|
| 9 |
+
from .reference import SynapseConfig, SynapseState
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
"BuilderSynapseStepper",
|
| 13 |
+
"LEARNING_CONTRACT_ID",
|
| 14 |
+
"LEARNING_CONTRACT_STATUS",
|
| 15 |
+
"PLASTICITY_SEMANTICS",
|
| 16 |
+
"SynapseConfig",
|
| 17 |
+
"SynapseState",
|
| 18 |
+
]
|
src/acceleration/cuda/plasticity/backend.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CUDA synaptic kernels extracted from the Playground engineering path.
|
| 2 |
+
|
| 3 |
+
Semantics remain NON_CANONICAL_DRAFT until the canonical learning contract is
|
| 4 |
+
implemented by both CPU and CUDA paths.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import ctypes
|
| 10 |
+
import math
|
| 11 |
+
from collections.abc import Sequence
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
from ..driver import CudaDriver, DriverModule
|
| 15 |
+
from ..errors import CudaDriverError
|
| 16 |
+
from ..memory import DeviceAllocation
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class BuilderSynapseStepper:
|
| 20 |
+
"""Reuse bounded buffers for emission and per-tick recovery/decay."""
|
| 21 |
+
|
| 22 |
+
def __init__(self, driver: CudaDriver, loaded: DriverModule, capacity: int) -> None:
|
| 23 |
+
if type(capacity) is not int or not 1 <= capacity <= 20000:
|
| 24 |
+
raise ValueError("invalid CUDA synaptic capacity")
|
| 25 |
+
self.driver, self.capacity = driver, capacity
|
| 26 |
+
self.emit_kernel = driver.kernel(loaded, "pan_synaptic_emit")
|
| 27 |
+
self.recover_kernel = driver.kernel(loaded, "pan_synaptic_recover")
|
| 28 |
+
self.reward_kernel = driver.kernel(loaded, "pan_synaptic_reward")
|
| 29 |
+
self.plasticity_kernel = driver.kernel(loaded, "pan_synaptic_plasticity")
|
| 30 |
+
self.scale_kernel = driver.kernel(loaded, "pan_synaptic_scale")
|
| 31 |
+
self.device: dict[str, DeviceAllocation] = {}
|
| 32 |
+
self.host: dict[str, Any] = {
|
| 33 |
+
"input": (ctypes.c_double * (12 * capacity))(),
|
| 34 |
+
"output": (ctypes.c_double * (3 * capacity))(),
|
| 35 |
+
}
|
| 36 |
+
self.emitted_events = 0
|
| 37 |
+
self.recovery_calls = 0
|
| 38 |
+
self.reward_calls = 0
|
| 39 |
+
self.plasticity_calls = 0
|
| 40 |
+
try:
|
| 41 |
+
for name in self.host:
|
| 42 |
+
self.device[name] = driver.alloc_device(ctypes.sizeof(self.host[name]))
|
| 43 |
+
except BaseException:
|
| 44 |
+
self.close()
|
| 45 |
+
raise
|
| 46 |
+
|
| 47 |
+
def close(self) -> None:
|
| 48 |
+
for allocation in reversed(list(self.device.values())):
|
| 49 |
+
try:
|
| 50 |
+
self.driver.free_device(allocation)
|
| 51 |
+
except CudaDriverError:
|
| 52 |
+
pass
|
| 53 |
+
self.device.clear()
|
| 54 |
+
|
| 55 |
+
def _execute(
|
| 56 |
+
self,
|
| 57 |
+
rows: list[tuple[float, float, float, float, float]],
|
| 58 |
+
*,
|
| 59 |
+
stp: bool,
|
| 60 |
+
first: float,
|
| 61 |
+
second: float,
|
| 62 |
+
kernel: DriverModule,
|
| 63 |
+
) -> list[tuple[float, float]]:
|
| 64 |
+
count = len(rows)
|
| 65 |
+
if (
|
| 66 |
+
count > self.capacity
|
| 67 |
+
or any(
|
| 68 |
+
len(row) != 5 or any(not math.isfinite(x) for x in row) for row in rows
|
| 69 |
+
)
|
| 70 |
+
or not all(math.isfinite(x) for x in (first, second))
|
| 71 |
+
):
|
| 72 |
+
raise ValueError("invalid CUDA synaptic shape/non-finite input")
|
| 73 |
+
if not count:
|
| 74 |
+
return []
|
| 75 |
+
self.host["input"][: 5 * count] = [x for row in rows for x in row]
|
| 76 |
+
self.driver.copy_host_to_device(
|
| 77 |
+
self.device["input"], self.host["input"], size_bytes=5 * count * 8
|
| 78 |
+
)
|
| 79 |
+
args: list[object] = [
|
| 80 |
+
ctypes.c_uint32(count),
|
| 81 |
+
ctypes.c_uint32(bool(stp)),
|
| 82 |
+
ctypes.c_double(first),
|
| 83 |
+
ctypes.c_double(second),
|
| 84 |
+
ctypes.c_uint64(self.device["input"].ptr),
|
| 85 |
+
ctypes.c_uint64(self.device["output"].ptr),
|
| 86 |
+
]
|
| 87 |
+
self.driver.launch_kernel(
|
| 88 |
+
kernel, grid=((count + 127) // 128, 1, 1), block=(128, 1, 1), arguments=args
|
| 89 |
+
)
|
| 90 |
+
self.driver.synchronize()
|
| 91 |
+
self.driver.copy_device_to_host(
|
| 92 |
+
self.host["output"], self.device["output"], size_bytes=2 * count * 8
|
| 93 |
+
)
|
| 94 |
+
values = list(self.host["output"][: 2 * count])
|
| 95 |
+
if any(not math.isfinite(x) for x in values):
|
| 96 |
+
raise ValueError("non-finite CUDA synaptic output")
|
| 97 |
+
return [(values[2 * i], values[2 * i + 1]) for i in range(count)]
|
| 98 |
+
|
| 99 |
+
def emit(
|
| 100 |
+
self,
|
| 101 |
+
rows: list[tuple[float, float, float, float, float]],
|
| 102 |
+
*,
|
| 103 |
+
stp: bool,
|
| 104 |
+
gaba: float,
|
| 105 |
+
ratio: float,
|
| 106 |
+
) -> list[tuple[float, float]]:
|
| 107 |
+
if (
|
| 108 |
+
not math.isfinite(gaba)
|
| 109 |
+
or gaba < 0
|
| 110 |
+
or not math.isfinite(ratio)
|
| 111 |
+
or ratio <= 0
|
| 112 |
+
):
|
| 113 |
+
raise ValueError("invalid CUDA inhibitory scaling")
|
| 114 |
+
for _weight, available, amplitude, draw, inhibitory in rows:
|
| 115 |
+
if (
|
| 116 |
+
not 0 <= available <= 1
|
| 117 |
+
or not 0 <= amplitude <= 1
|
| 118 |
+
or not 0 <= draw < 1
|
| 119 |
+
or inhibitory not in (0, 1)
|
| 120 |
+
):
|
| 121 |
+
raise ValueError("invalid CUDA STP/amplitude/RNG descriptor")
|
| 122 |
+
result = self._execute(
|
| 123 |
+
rows, stp=stp, first=gaba, second=ratio, kernel=self.emit_kernel
|
| 124 |
+
)
|
| 125 |
+
self.emitted_events += len(rows)
|
| 126 |
+
return result
|
| 127 |
+
|
| 128 |
+
def recover(
|
| 129 |
+
self,
|
| 130 |
+
weights: list[float],
|
| 131 |
+
resources: list[float],
|
| 132 |
+
*,
|
| 133 |
+
stp: bool,
|
| 134 |
+
decay: float,
|
| 135 |
+
maximum: float,
|
| 136 |
+
) -> list[tuple[float, float]]:
|
| 137 |
+
if (
|
| 138 |
+
len(weights) != len(resources)
|
| 139 |
+
or not 0 <= decay <= 1
|
| 140 |
+
or not 0 < maximum <= 100
|
| 141 |
+
or any(not 0 <= x <= 1 for x in resources)
|
| 142 |
+
):
|
| 143 |
+
raise ValueError("invalid CUDA recovery/decay parameters")
|
| 144 |
+
result = self._execute(
|
| 145 |
+
[(w, r, 0.0, 0.0, 0.0) for w, r in zip(weights, resources)],
|
| 146 |
+
stp=stp,
|
| 147 |
+
first=decay,
|
| 148 |
+
second=maximum,
|
| 149 |
+
kernel=self.recover_kernel,
|
| 150 |
+
)
|
| 151 |
+
if result:
|
| 152 |
+
self.recovery_calls += 1
|
| 153 |
+
return result
|
| 154 |
+
|
| 155 |
+
def reward(
|
| 156 |
+
self,
|
| 157 |
+
weights: list[float],
|
| 158 |
+
eligibility: list[float],
|
| 159 |
+
ages: list[int],
|
| 160 |
+
*,
|
| 161 |
+
scale: float,
|
| 162 |
+
reward: float,
|
| 163 |
+
maximum: float,
|
| 164 |
+
window: int,
|
| 165 |
+
respect_window: bool,
|
| 166 |
+
) -> list[float]:
|
| 167 |
+
if (
|
| 168 |
+
len(weights) != len(eligibility)
|
| 169 |
+
or len(weights) != len(ages)
|
| 170 |
+
or not 0 < maximum <= 100
|
| 171 |
+
or type(window) is not int
|
| 172 |
+
or window < 0
|
| 173 |
+
or any(type(age) is not int for age in ages)
|
| 174 |
+
):
|
| 175 |
+
raise ValueError("invalid CUDA live reward shape/window")
|
| 176 |
+
rows = [
|
| 177 |
+
(w, e, float(age), float(window), maximum)
|
| 178 |
+
for w, e, age in zip(weights, eligibility, ages)
|
| 179 |
+
]
|
| 180 |
+
values = self._execute(
|
| 181 |
+
rows,
|
| 182 |
+
stp=respect_window,
|
| 183 |
+
first=scale,
|
| 184 |
+
second=reward,
|
| 185 |
+
kernel=self.reward_kernel,
|
| 186 |
+
)
|
| 187 |
+
if values:
|
| 188 |
+
self.reward_calls += 1
|
| 189 |
+
return [w for w, _ in values]
|
| 190 |
+
|
| 191 |
+
def plasticity(
|
| 192 |
+
self,
|
| 193 |
+
rows: Sequence[tuple[float, ...]],
|
| 194 |
+
*,
|
| 195 |
+
pair: bool,
|
| 196 |
+
triplet: bool,
|
| 197 |
+
eligibility_active: bool,
|
| 198 |
+
decay: float,
|
| 199 |
+
learning: float,
|
| 200 |
+
) -> list[tuple[float, float, int]]:
|
| 201 |
+
count = len(rows)
|
| 202 |
+
if (
|
| 203 |
+
count > self.capacity
|
| 204 |
+
or not math.isfinite(decay)
|
| 205 |
+
or not 0 <= decay <= 1
|
| 206 |
+
or not math.isfinite(learning)
|
| 207 |
+
or learning < 0
|
| 208 |
+
or any(
|
| 209 |
+
len(row) != 12 or any(not math.isfinite(x) for x in row) for row in rows
|
| 210 |
+
)
|
| 211 |
+
):
|
| 212 |
+
raise ValueError("invalid CUDA plasticity input")
|
| 213 |
+
for row in rows:
|
| 214 |
+
if (
|
| 215 |
+
row[7] not in (0, 1)
|
| 216 |
+
or row[8] not in (0, 1)
|
| 217 |
+
or row[9] == row[10]
|
| 218 |
+
or any(x != int(x) for x in (row[2], row[9], row[10], row[11]))
|
| 219 |
+
):
|
| 220 |
+
raise ValueError("invalid CUDA plasticity event/index")
|
| 221 |
+
if not count:
|
| 222 |
+
return []
|
| 223 |
+
self.host["input"][: 12 * count] = [x for row in rows for x in row]
|
| 224 |
+
self.driver.copy_host_to_device(
|
| 225 |
+
self.device["input"], self.host["input"], size_bytes=12 * count * 8
|
| 226 |
+
)
|
| 227 |
+
flags = int(pair) | int(triplet) * 2 | int(eligibility_active) * 4
|
| 228 |
+
args: list[object] = [
|
| 229 |
+
ctypes.c_uint32(count),
|
| 230 |
+
ctypes.c_uint32(flags),
|
| 231 |
+
ctypes.c_double(decay),
|
| 232 |
+
ctypes.c_double(learning),
|
| 233 |
+
ctypes.c_uint64(self.device["input"].ptr),
|
| 234 |
+
ctypes.c_uint64(self.device["output"].ptr),
|
| 235 |
+
]
|
| 236 |
+
self.driver.launch_kernel(
|
| 237 |
+
self.plasticity_kernel,
|
| 238 |
+
grid=((count + 127) // 128, 1, 1),
|
| 239 |
+
block=(128, 1, 1),
|
| 240 |
+
arguments=args,
|
| 241 |
+
)
|
| 242 |
+
self.driver.synchronize()
|
| 243 |
+
self.driver.copy_device_to_host(
|
| 244 |
+
self.host["output"], self.device["output"], size_bytes=3 * count * 8
|
| 245 |
+
)
|
| 246 |
+
values = list(self.host["output"][: 3 * count])
|
| 247 |
+
if any(not math.isfinite(x) for x in values) or any(
|
| 248 |
+
x != int(x) for x in values[2::3]
|
| 249 |
+
):
|
| 250 |
+
raise ValueError("invalid CUDA plasticity output")
|
| 251 |
+
self.plasticity_calls += 1
|
| 252 |
+
return [
|
| 253 |
+
(values[3 * i], values[3 * i + 1], int(values[3 * i + 2]))
|
| 254 |
+
for i in range(count)
|
| 255 |
+
]
|
| 256 |
+
|
| 257 |
+
def scale_weights(
|
| 258 |
+
self, weights: list[float], *, factor: float, maximum: float
|
| 259 |
+
) -> list[float]:
|
| 260 |
+
if not math.isfinite(factor) or factor < 0 or not 0 < maximum <= 100:
|
| 261 |
+
raise ValueError("invalid CUDA weight scaling")
|
| 262 |
+
rows = [(w, 0.0, 0.0, 0.0, 0.0) for w in weights]
|
| 263 |
+
return [
|
| 264 |
+
w
|
| 265 |
+
for w, _ in self._execute(
|
| 266 |
+
rows, stp=False, first=factor, second=maximum, kernel=self.scale_kernel
|
| 267 |
+
)
|
| 268 |
+
]
|
src/acceleration/cuda/plasticity/contracts.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Wave-4 plasticity capability markers with Wave-7 draft linkage."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Final
|
| 6 |
+
|
| 7 |
+
from src.learning.contract import (
|
| 8 |
+
LEARNING_CONTRACT_ID as LEARNING_DRAFT_CONTRACT_ID,
|
| 9 |
+
)
|
| 10 |
+
from src.learning.contract import (
|
| 11 |
+
LEARNING_CONTRACT_STATUS as LEARNING_DRAFT_CONTRACT_STATUS,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
PLASTICITY_SEMANTICS: Final[str] = "NON_CANONICAL_DRAFT"
|
| 15 |
+
|
| 16 |
+
# Frozen Wave-4 compatibility boundary. These values are intentionally retained
|
| 17 |
+
# so the extracted CUDA backend does not retroactively change its acceptance
|
| 18 |
+
# contract while Wave 7 is still drafting the common learning semantics.
|
| 19 |
+
LEARNING_CONTRACT_ID: Final[str] = "mhrn-learning-synapse-v1"
|
| 20 |
+
LEARNING_CONTRACT_STATUS: Final[str] = "ALIGNMENT_PENDING"
|
| 21 |
+
|
| 22 |
+
__all__ = [
|
| 23 |
+
"LEARNING_CONTRACT_ID",
|
| 24 |
+
"LEARNING_CONTRACT_STATUS",
|
| 25 |
+
"LEARNING_DRAFT_CONTRACT_ID",
|
| 26 |
+
"LEARNING_DRAFT_CONTRACT_STATUS",
|
| 27 |
+
"PLASTICITY_SEMANTICS",
|
| 28 |
+
]
|
src/acceleration/cuda/plasticity/kernel.cu
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Builder emission algebra; host supplies traversal-ordered random draws.
|
| 2 |
+
extern "C" __global__ void pan_synaptic_emit(unsigned count, unsigned stp,
|
| 3 |
+
double gaba, double ratio, const double* input, double* output) {
|
| 4 |
+
unsigned i=blockIdx.x*blockDim.x+threadIdx.x; if(i>=count) return;
|
| 5 |
+
const double* x=input+5*i;
|
| 6 |
+
double amplitude=x[0], available=x[1];
|
| 7 |
+
if(x[4]!=0.0) amplitude=-fabs(amplitude)*gaba/ratio;
|
| 8 |
+
amplitude*=x[2];
|
| 9 |
+
if(stp) {
|
| 10 |
+
amplitude*=x[3]<fmin(0.95,0.25+0.7*available)?1.0:0.0;
|
| 11 |
+
available=fmax(0.1,available*0.72);
|
| 12 |
+
}
|
| 13 |
+
output[2*i]=amplitude; output[2*i+1]=available;
|
| 14 |
+
}
|
| 15 |
+
extern "C" __global__ void pan_synaptic_recover(unsigned count, unsigned stp,
|
| 16 |
+
double decay, double maximum, const double* input, double* output) {
|
| 17 |
+
unsigned i=blockIdx.x*blockDim.x+threadIdx.x; if(i>=count) return;
|
| 18 |
+
double weight=input[5*i], available=input[5*i+1];
|
| 19 |
+
if(stp) available=fmin(1.0,available+0.025);
|
| 20 |
+
if(decay!=0.0) weight*=fmax(0.0,1.0-decay);
|
| 21 |
+
output[2*i]=fmin(maximum,fmax(0.0,weight)); output[2*i+1]=available;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// Live target/posture rewards keep the Builder's eligibility-window contract.
|
| 25 |
+
extern "C" __global__ void pan_synaptic_reward(unsigned count, unsigned filter_age,
|
| 26 |
+
double scale, double reward, const double* input, double* output) {
|
| 27 |
+
unsigned i=blockIdx.x*blockDim.x+threadIdx.x; if(i>=count) return;
|
| 28 |
+
const double* x=input+5*i;
|
| 29 |
+
double weight=x[0];
|
| 30 |
+
if(!filter_age || x[2]<=x[3]) weight=fmin(x[4],fmax(0.0,weight+scale*x[1]*reward));
|
| 31 |
+
output[2*i]=weight; output[2*i+1]=x[1];
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// Independent edges reproduce the original ascending-neuron event order.
|
| 35 |
+
extern "C" __global__ void pan_synaptic_plasticity(unsigned count, unsigned flags,
|
| 36 |
+
double decay, double learning, const double* input, double* output) {
|
| 37 |
+
unsigned i=blockIdx.x*blockDim.x+threadIdx.x; if(i>=count) return;
|
| 38 |
+
// weight, eligibility, eligibility time, pre/post deltas, decayed traces,
|
| 39 |
+
// spike flags, source/target IDs, current tick.
|
| 40 |
+
const double* x=input+12*i;
|
| 41 |
+
double weight=x[0], eligibility=x[1]*decay, last=x[2];
|
| 42 |
+
bool pre=x[7]!=0.0, post=x[8]!=0.0, pre_first=x[9]<x[10];
|
| 43 |
+
for(unsigned event=0;event<2;++event) {
|
| 44 |
+
bool source_event=(event==0)==pre_first;
|
| 45 |
+
if(source_event && pre) {
|
| 46 |
+
if((flags&1) && x[4]>0.0 && x[4]<=20.0) weight=fmax(0.0,weight-0.08*learning);
|
| 47 |
+
if(flags&2) weight=fmax(0.0,weight-0.04*(x[6]+((!pre_first && post)?1.0:0.0)));
|
| 48 |
+
if(flags&4) {eligibility-=0.5;last=x[11];}
|
| 49 |
+
} else if(!source_event && post) {
|
| 50 |
+
if((flags&1) && x[3]>0.0 && x[3]<=20.0) weight=fmin(100.0,weight+0.1*learning);
|
| 51 |
+
if(flags&2) weight=fmin(100.0,fmax(0.0,weight+0.06*(x[5]+((pre_first && pre)?1.0:0.0))+0.025*x[6]));
|
| 52 |
+
if(flags&4) {eligibility+=1.0;last=x[11];}
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
output[3*i]=weight;output[3*i+1]=eligibility;output[3*i+2]=last;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
extern "C" __global__ void pan_synaptic_scale(unsigned count, unsigned unused,
|
| 59 |
+
double factor, double maximum, const double* input, double* output) {
|
| 60 |
+
unsigned i=blockIdx.x*blockDim.x+threadIdx.x;if(i>=count)return;
|
| 61 |
+
output[2*i]=fmax(0.0,fmin(maximum,input[5*i]*factor));output[2*i+1]=0.0;
|
| 62 |
+
}
|
src/acceleration/cuda/plasticity/reference.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ordered scalar synapse oracle for the canonical bounded CUDA reference."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from typing import TYPE_CHECKING
|
| 8 |
+
|
| 9 |
+
from src.runtime.determinism import (
|
| 10 |
+
release_uniform as release_uniform,
|
| 11 |
+
)
|
| 12 |
+
from src.runtime.determinism import (
|
| 13 |
+
ring_slot as ring_slot,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
if TYPE_CHECKING:
|
| 17 |
+
from ..recurrent.backend import RecurrentInputs
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass(frozen=True)
|
| 21 |
+
class SynapseConfig:
|
| 22 |
+
stdp: bool = True
|
| 23 |
+
stp: bool = True
|
| 24 |
+
reward_modulated: bool = True
|
| 25 |
+
seed: int = 12345
|
| 26 |
+
learning_rate: float = 0.2
|
| 27 |
+
weight_decay: float = 0.001
|
| 28 |
+
weight_max: float = 20.0
|
| 29 |
+
eligibility_tau: float = 100.0
|
| 30 |
+
credit_window: int = 64
|
| 31 |
+
td_lambda: float = 0.9
|
| 32 |
+
gamma: float = 0.99
|
| 33 |
+
|
| 34 |
+
def parameters(self) -> list[float]:
|
| 35 |
+
return [
|
| 36 |
+
float(self.stdp),
|
| 37 |
+
float(self.stp),
|
| 38 |
+
float(self.reward_modulated),
|
| 39 |
+
float(self.seed),
|
| 40 |
+
self.learning_rate,
|
| 41 |
+
self.weight_decay,
|
| 42 |
+
self.weight_max,
|
| 43 |
+
self.eligibility_tau,
|
| 44 |
+
float(self.credit_window),
|
| 45 |
+
self.td_lambda,
|
| 46 |
+
self.gamma,
|
| 47 |
+
]
|
| 48 |
+
|
| 49 |
+
def validate(self) -> None:
|
| 50 |
+
if any(
|
| 51 |
+
type(value) is not bool
|
| 52 |
+
for value in (self.stdp, self.stp, self.reward_modulated)
|
| 53 |
+
):
|
| 54 |
+
raise ValueError("synapse mode flags must be boolean")
|
| 55 |
+
if type(self.seed) is not int or not 0 <= self.seed <= 0xFFFFFFFF:
|
| 56 |
+
raise ValueError("synapse seed must be uint32")
|
| 57 |
+
if type(self.credit_window) is not int or not 0 <= self.credit_window <= 2000:
|
| 58 |
+
raise ValueError("credit_window must be in [0,2000]")
|
| 59 |
+
for name, value, low, high in (
|
| 60 |
+
("learning_rate", self.learning_rate, 0.0, 1.0),
|
| 61 |
+
("weight_decay", self.weight_decay, 0.0, 1.0),
|
| 62 |
+
("weight_max", self.weight_max, 0.001, 100.0),
|
| 63 |
+
("eligibility_tau", self.eligibility_tau, 0.001, 10000.0),
|
| 64 |
+
("td_lambda", self.td_lambda, 0.0, 1.0),
|
| 65 |
+
("gamma", self.gamma, 0.0, 1.0),
|
| 66 |
+
):
|
| 67 |
+
if (
|
| 68 |
+
isinstance(value, bool)
|
| 69 |
+
or not math.isfinite(value)
|
| 70 |
+
or not low <= value <= high
|
| 71 |
+
):
|
| 72 |
+
raise ValueError(f"invalid synapse {name}")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class SynapseState:
|
| 76 |
+
def __init__(self, inputs: RecurrentInputs, config: SynapseConfig) -> None:
|
| 77 |
+
self.inputs = inputs
|
| 78 |
+
self.config = config
|
| 79 |
+
self.weights = list(inputs.weights)
|
| 80 |
+
self.eligibility = [0.0] * len(inputs.sources)
|
| 81 |
+
self.available = [1.0] * len(inputs.sources)
|
| 82 |
+
self.last_event = [-10_000_000] * len(inputs.sources)
|
| 83 |
+
self.last_spike = [-10_000_000] * inputs.n_neurons
|
| 84 |
+
self.ring_size = max(inputs.delays, default=1) + 1
|
| 85 |
+
self.emitted = [[0.0] * len(inputs.sources) for _ in range(self.ring_size)]
|
| 86 |
+
|
| 87 |
+
def incoming(self, tick: int, neuron: int) -> float:
|
| 88 |
+
# Kept for external callers; the membrane oracle sums in edge order.
|
| 89 |
+
return sum(
|
| 90 |
+
self.edge_current(tick, edge)
|
| 91 |
+
for edge in range(
|
| 92 |
+
self.inputs.offsets[neuron], self.inputs.offsets[neuron + 1]
|
| 93 |
+
)
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
def edge_current(self, tick: int, edge: int) -> float:
|
| 97 |
+
previous = tick - self.inputs.delays[edge]
|
| 98 |
+
return (
|
| 99 |
+
self.emitted[ring_slot(previous, self.ring_size)][edge]
|
| 100 |
+
if previous >= 0
|
| 101 |
+
else 0.0
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
def update(self, tick: int, spikes: list[int], reward: float) -> None:
|
| 105 |
+
c, inputs = self.config, self.inputs
|
| 106 |
+
for target in range(inputs.n_neurons):
|
| 107 |
+
for edge in range(inputs.offsets[target], inputs.offsets[target + 1]):
|
| 108 |
+
source = inputs.sources[edge]
|
| 109 |
+
pre, post = spikes[source], spikes[target]
|
| 110 |
+
weight = self.weights[edge]
|
| 111 |
+
eligibility = self.eligibility[edge] * math.exp(
|
| 112 |
+
-inputs.dt_ms / c.eligibility_tau
|
| 113 |
+
)
|
| 114 |
+
# Match ascending-neuron pair-rule ordering, including self edges.
|
| 115 |
+
operations = ("pre", "post") if source <= target else ("post", "pre")
|
| 116 |
+
for operation in operations:
|
| 117 |
+
if operation == "pre" and pre:
|
| 118 |
+
if c.stdp and 0 < tick - self.last_spike[target] <= 20:
|
| 119 |
+
weight = max(0.0, weight - 0.08)
|
| 120 |
+
eligibility -= 0.5
|
| 121 |
+
if operation == "post" and post:
|
| 122 |
+
if c.stdp and 0 < tick - self.last_spike[source] <= 20:
|
| 123 |
+
weight = min(100.0, weight + 0.1)
|
| 124 |
+
eligibility += 1.0
|
| 125 |
+
if pre or post:
|
| 126 |
+
self.last_event[edge] = tick
|
| 127 |
+
if (
|
| 128 |
+
c.reward_modulated
|
| 129 |
+
and tick - self.last_event[edge] <= c.credit_window
|
| 130 |
+
):
|
| 131 |
+
weight = min(
|
| 132 |
+
100.0,
|
| 133 |
+
max(
|
| 134 |
+
0.0,
|
| 135 |
+
weight
|
| 136 |
+
+ c.learning_rate
|
| 137 |
+
* c.td_lambda
|
| 138 |
+
* c.gamma
|
| 139 |
+
* eligibility
|
| 140 |
+
* reward,
|
| 141 |
+
),
|
| 142 |
+
)
|
| 143 |
+
amplitude = weight if pre else 0.0
|
| 144 |
+
available = self.available[edge]
|
| 145 |
+
if c.stp:
|
| 146 |
+
if pre:
|
| 147 |
+
amplitude *= float(
|
| 148 |
+
release_uniform(c.seed, tick, edge)
|
| 149 |
+
< min(0.95, 0.25 + 0.7 * available)
|
| 150 |
+
)
|
| 151 |
+
available = max(0.1, available * 0.72)
|
| 152 |
+
available = min(1.0, available + 0.025)
|
| 153 |
+
self.available[edge] = available
|
| 154 |
+
self.emitted[ring_slot(tick, self.ring_size)][edge] = amplitude
|
| 155 |
+
self.weights[edge] = min(
|
| 156 |
+
c.weight_max, max(0.0, weight * (1.0 - c.weight_decay))
|
| 157 |
+
)
|
| 158 |
+
self.eligibility[edge] = eligibility
|
| 159 |
+
for neuron, spike in enumerate(spikes):
|
| 160 |
+
if spike:
|
| 161 |
+
self.last_spike[neuron] = tick
|
src/acceleration/cuda/preflight.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Backend-neutral CUDA cooperative launch preflight contracts."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass(frozen=True, slots=True)
|
| 10 |
+
class CooperativePreflight:
|
| 11 |
+
"""Host/device limits required for a cooperative persistent launch."""
|
| 12 |
+
|
| 13 |
+
cooperative_launch: bool
|
| 14 |
+
multiprocessor_count: int
|
| 15 |
+
active_blocks_per_sm: int
|
| 16 |
+
block_size: int
|
| 17 |
+
required_blocks: int
|
| 18 |
+
resident_block_capacity: int
|
| 19 |
+
launch_fits: bool
|
| 20 |
+
n_neurons: int
|
| 21 |
+
|
| 22 |
+
def to_mapping(self) -> dict[str, object]:
|
| 23 |
+
return {
|
| 24 |
+
"cooperative_launch": self.cooperative_launch,
|
| 25 |
+
"multiprocessor_count": self.multiprocessor_count,
|
| 26 |
+
"active_blocks_per_sm": self.active_blocks_per_sm,
|
| 27 |
+
"block_size": self.block_size,
|
| 28 |
+
"required_blocks": self.required_blocks,
|
| 29 |
+
"resident_block_capacity": self.resident_block_capacity,
|
| 30 |
+
"launch_fits": self.launch_fits,
|
| 31 |
+
"n_neurons": self.n_neurons,
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def validate_cuda_block_size(block_size: int) -> int:
|
| 36 |
+
"""Validate a legal one-dimensional CUDA block size."""
|
| 37 |
+
|
| 38 |
+
if isinstance(block_size, bool) or not isinstance(block_size, int):
|
| 39 |
+
raise ValueError("block_size must be an integer")
|
| 40 |
+
if not 1 <= block_size <= 1024:
|
| 41 |
+
raise ValueError("block_size must be in [1, 1024]")
|
| 42 |
+
if block_size % 32 != 0:
|
| 43 |
+
raise ValueError("block_size must be a multiple of 32")
|
| 44 |
+
return block_size
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def cooperative_capacity(
|
| 48 |
+
*,
|
| 49 |
+
n_neurons: int,
|
| 50 |
+
block_size: int,
|
| 51 |
+
multiprocessor_count: int,
|
| 52 |
+
active_blocks_per_sm: int,
|
| 53 |
+
cooperative_launch: bool = True,
|
| 54 |
+
) -> CooperativePreflight:
|
| 55 |
+
"""Pure helper used by CI to verify occupancy-bound grid logic."""
|
| 56 |
+
|
| 57 |
+
if (
|
| 58 |
+
min(
|
| 59 |
+
n_neurons,
|
| 60 |
+
block_size,
|
| 61 |
+
multiprocessor_count,
|
| 62 |
+
active_blocks_per_sm,
|
| 63 |
+
)
|
| 64 |
+
<= 0
|
| 65 |
+
):
|
| 66 |
+
raise ValueError("cooperative capacity inputs must be positive")
|
| 67 |
+
required = math.ceil(n_neurons / block_size)
|
| 68 |
+
capacity = multiprocessor_count * active_blocks_per_sm
|
| 69 |
+
return CooperativePreflight(
|
| 70 |
+
cooperative_launch=cooperative_launch,
|
| 71 |
+
multiprocessor_count=multiprocessor_count,
|
| 72 |
+
active_blocks_per_sm=active_blocks_per_sm,
|
| 73 |
+
block_size=block_size,
|
| 74 |
+
required_blocks=required,
|
| 75 |
+
resident_block_capacity=capacity,
|
| 76 |
+
launch_fits=cooperative_launch and required <= capacity,
|
| 77 |
+
n_neurons=n_neurons,
|
| 78 |
+
)
|
src/acceleration/cuda/ptxas.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical PTX assembly and resource-report helpers."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import re
|
| 6 |
+
import shutil
|
| 7 |
+
import subprocess
|
| 8 |
+
import tempfile
|
| 9 |
+
from dataclasses import dataclass
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
from .errors import CudaDriverError, CudaRuntimeUnavailable
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass(frozen=True, slots=True)
|
| 16 |
+
class PtxasReport:
|
| 17 |
+
"""Measured resource report emitted by ptxas."""
|
| 18 |
+
|
| 19 |
+
target_sm: str
|
| 20 |
+
registers: int | None
|
| 21 |
+
shared_bytes: int | None
|
| 22 |
+
constant_bytes: int | None
|
| 23 |
+
stack_bytes: int | None
|
| 24 |
+
spill_store_bytes: int | None
|
| 25 |
+
spill_load_bytes: int | None
|
| 26 |
+
cubin_path: Path
|
| 27 |
+
verbose_output: str
|
| 28 |
+
|
| 29 |
+
def to_mapping(self) -> dict[str, object]:
|
| 30 |
+
return {
|
| 31 |
+
"target_sm": self.target_sm,
|
| 32 |
+
"registers": self.registers,
|
| 33 |
+
"shared_bytes": self.shared_bytes,
|
| 34 |
+
"constant_bytes": self.constant_bytes,
|
| 35 |
+
"stack_bytes": self.stack_bytes,
|
| 36 |
+
"spill_store_bytes": self.spill_store_bytes,
|
| 37 |
+
"spill_load_bytes": self.spill_load_bytes,
|
| 38 |
+
"cubin_path": str(self.cubin_path),
|
| 39 |
+
"verbose_output": self.verbose_output,
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
_REGISTER_RE = re.compile(r"Used\s+(\d+)\s+registers")
|
| 44 |
+
_SMEM_RE = re.compile(r"(\d+)\s+bytes smem")
|
| 45 |
+
_CMEM_RE = re.compile(r"(\d+)\s+bytes cmem\[\d+\]")
|
| 46 |
+
_STACK_RE = re.compile(r"(\d+)\s+bytes stack frame")
|
| 47 |
+
_SPILL_STORE_RE = re.compile(r"(\d+)\s+bytes spill stores")
|
| 48 |
+
_SPILL_LOAD_RE = re.compile(r"(\d+)\s+bytes spill loads")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _first_int(pattern: re.Pattern[str], text: str) -> int | None:
|
| 52 |
+
match = pattern.search(text)
|
| 53 |
+
return int(match.group(1)) if match else None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def parse_ptxas_verbose(
|
| 57 |
+
verbose_output: str,
|
| 58 |
+
*,
|
| 59 |
+
target_sm: str,
|
| 60 |
+
cubin_path: Path,
|
| 61 |
+
) -> PtxasReport:
|
| 62 |
+
"""Parse stable resource fields from ptxas verbose diagnostics."""
|
| 63 |
+
|
| 64 |
+
constant_values = [int(value) for value in _CMEM_RE.findall(verbose_output)]
|
| 65 |
+
return PtxasReport(
|
| 66 |
+
target_sm=target_sm,
|
| 67 |
+
registers=_first_int(_REGISTER_RE, verbose_output),
|
| 68 |
+
shared_bytes=_first_int(_SMEM_RE, verbose_output),
|
| 69 |
+
constant_bytes=sum(constant_values) if constant_values else None,
|
| 70 |
+
stack_bytes=_first_int(_STACK_RE, verbose_output),
|
| 71 |
+
spill_store_bytes=_first_int(_SPILL_STORE_RE, verbose_output),
|
| 72 |
+
spill_load_bytes=_first_int(_SPILL_LOAD_RE, verbose_output),
|
| 73 |
+
cubin_path=cubin_path,
|
| 74 |
+
verbose_output=verbose_output,
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def assemble_ptx(
|
| 79 |
+
ptx_source: str,
|
| 80 |
+
*,
|
| 81 |
+
target_sm: str = "sm_86",
|
| 82 |
+
output_dir: Path | None = None,
|
| 83 |
+
ptxas: str = "ptxas",
|
| 84 |
+
) -> PtxasReport:
|
| 85 |
+
"""Assemble generated PTX and return measured ptxas resources."""
|
| 86 |
+
|
| 87 |
+
executable = shutil.which(ptxas)
|
| 88 |
+
if executable is None:
|
| 89 |
+
raise CudaRuntimeUnavailable(f"{ptxas} is not available on PATH")
|
| 90 |
+
|
| 91 |
+
root = output_dir or Path(tempfile.mkdtemp(prefix="mhrn-ptxas-"))
|
| 92 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 93 |
+
ptx_path = root / "pan_gate_kernel.ptx"
|
| 94 |
+
cubin_path = root / "pan_gate_kernel.cubin"
|
| 95 |
+
ptx_path.write_text(ptx_source, encoding="utf-8")
|
| 96 |
+
|
| 97 |
+
process = subprocess.run(
|
| 98 |
+
[
|
| 99 |
+
executable,
|
| 100 |
+
"--verbose",
|
| 101 |
+
f"--gpu-name={target_sm}",
|
| 102 |
+
str(ptx_path),
|
| 103 |
+
"--output-file",
|
| 104 |
+
str(cubin_path),
|
| 105 |
+
],
|
| 106 |
+
check=False,
|
| 107 |
+
capture_output=True,
|
| 108 |
+
text=True,
|
| 109 |
+
)
|
| 110 |
+
verbose = "\n".join(part for part in (process.stdout, process.stderr) if part)
|
| 111 |
+
if process.returncode != 0:
|
| 112 |
+
raise CudaDriverError(
|
| 113 |
+
"ptxas rejected generated PTX " f"(exit={process.returncode}):\n{verbose}"
|
| 114 |
+
)
|
| 115 |
+
return parse_ptxas_verbose(
|
| 116 |
+
verbose,
|
| 117 |
+
target_sm=target_sm,
|
| 118 |
+
cubin_path=cubin_path,
|
| 119 |
+
)
|
src/acceleration/cuda/recurrent/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical bounded recurrent CUDA package."""
|
| 2 |
+
|
| 3 |
+
from .backend import (
|
| 4 |
+
RecurrentInputs,
|
| 5 |
+
cpu_recurrent_reference,
|
| 6 |
+
execute_recurrent,
|
| 7 |
+
recurrent_fixture,
|
| 8 |
+
recurrent_parity,
|
| 9 |
+
validate_recurrent_inputs,
|
| 10 |
+
)
|
| 11 |
+
from .config import SUPPORTED_MODELS, CudaReferenceModelProfile
|
| 12 |
+
from .reference import CPUReferenceBackend
|
| 13 |
+
from .state import recurrent_inputs_from_mapping, recurrent_inputs_to_mapping
|
| 14 |
+
|
| 15 |
+
__all__ = [
|
| 16 |
+
"CPUReferenceBackend",
|
| 17 |
+
"CudaReferenceModelProfile",
|
| 18 |
+
"RecurrentInputs",
|
| 19 |
+
"SUPPORTED_MODELS",
|
| 20 |
+
"cpu_recurrent_reference",
|
| 21 |
+
"execute_recurrent",
|
| 22 |
+
"recurrent_fixture",
|
| 23 |
+
"recurrent_inputs_from_mapping",
|
| 24 |
+
"recurrent_inputs_to_mapping",
|
| 25 |
+
"recurrent_parity",
|
| 26 |
+
"validate_recurrent_inputs",
|
| 27 |
+
]
|
src/acceleration/cuda/recurrent/backend.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical bounded CUDA recurrent reference with explicit FP64 state.
|
| 2 |
+
|
| 3 |
+
The CUDA kernel is byte-identical to the previously hardware-verified
|
| 4 |
+
Playground kernel. This module changes ownership and interfaces, not kernel
|
| 5 |
+
semantics. PAN hyperstate, structural plasticity and live environment state are
|
| 6 |
+
outside this Wave-4 backend.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import ctypes
|
| 12 |
+
import math
|
| 13 |
+
import tempfile
|
| 14 |
+
from dataclasses import dataclass
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from typing import Any
|
| 17 |
+
|
| 18 |
+
from src.verification.parity import max_abs_error
|
| 19 |
+
|
| 20 |
+
from ..driver import CudaDriver
|
| 21 |
+
from ..errors import CudaDriverError
|
| 22 |
+
from ..memory import DeviceAllocation
|
| 23 |
+
from ..nvrtc import compile_cuda_source
|
| 24 |
+
from ..plasticity.reference import SynapseConfig, SynapseState
|
| 25 |
+
from ..preflight import validate_cuda_block_size
|
| 26 |
+
from .config import SUPPORTED_MODELS, cuda_reference_model_profile, step_reference
|
| 27 |
+
|
| 28 |
+
PARAMETER_NAMES = (
|
| 29 |
+
"v_rest",
|
| 30 |
+
"v_t",
|
| 31 |
+
"delta_t",
|
| 32 |
+
"tau_m_ms",
|
| 33 |
+
"tau_w_ms",
|
| 34 |
+
"a",
|
| 35 |
+
"b",
|
| 36 |
+
"threshold",
|
| 37 |
+
"reset",
|
| 38 |
+
"resistance",
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass(frozen=True)
|
| 43 |
+
class RecurrentInputs:
|
| 44 |
+
n_neurons: int
|
| 45 |
+
ticks: int
|
| 46 |
+
offsets: tuple[int, ...]
|
| 47 |
+
sources: tuple[int, ...]
|
| 48 |
+
delays: tuple[int, ...]
|
| 49 |
+
weights: tuple[float, ...]
|
| 50 |
+
external: tuple[float, ...]
|
| 51 |
+
voltage: tuple[float, ...]
|
| 52 |
+
adaptation: tuple[float, ...]
|
| 53 |
+
model: str = "lif"
|
| 54 |
+
dt_ms: float = 1.0
|
| 55 |
+
synapses: SynapseConfig | None = None
|
| 56 |
+
rewards: tuple[float, ...] = ()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _bounded_integer(value: object, lower: int, upper: int) -> bool:
|
| 60 |
+
return (
|
| 61 |
+
not isinstance(value, bool)
|
| 62 |
+
and isinstance(value, int)
|
| 63 |
+
and lower <= value <= upper
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _bounded_number(value: object) -> bool:
|
| 68 |
+
return (
|
| 69 |
+
not isinstance(value, bool)
|
| 70 |
+
and isinstance(value, (int, float))
|
| 71 |
+
and math.isfinite(value)
|
| 72 |
+
and abs(value) <= 1e6
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def validate_recurrent_inputs(inputs: RecurrentInputs) -> None:
|
| 77 |
+
for name, value, upper in (
|
| 78 |
+
("n_neurons", inputs.n_neurons, 4096),
|
| 79 |
+
("ticks", inputs.ticks, 2000),
|
| 80 |
+
):
|
| 81 |
+
if not _bounded_integer(value, 1, upper):
|
| 82 |
+
raise ValueError(f"{name} must be an integer in [1,{upper}]")
|
| 83 |
+
n, ticks = inputs.n_neurons, inputs.ticks
|
| 84 |
+
if inputs.synapses is not None:
|
| 85 |
+
inputs.synapses.validate()
|
| 86 |
+
if len(inputs.rewards) != ticks or any(
|
| 87 |
+
not _bounded_number(x) for x in inputs.rewards
|
| 88 |
+
):
|
| 89 |
+
raise ValueError("plastic execution requires finite reward per tick")
|
| 90 |
+
if not inputs.sources:
|
| 91 |
+
raise ValueError("plastic execution requires at least one synapse")
|
| 92 |
+
if any(weight < 0 for weight in inputs.weights):
|
| 93 |
+
raise ValueError("plastic weights must be nonnegative")
|
| 94 |
+
if n * ticks > 4_000_000:
|
| 95 |
+
raise ValueError("state history exceeds bounded reference capacity")
|
| 96 |
+
if inputs.model not in SUPPORTED_MODELS:
|
| 97 |
+
raise ValueError("unsupported recurrent membrane model")
|
| 98 |
+
if (
|
| 99 |
+
isinstance(inputs.dt_ms, bool)
|
| 100 |
+
or not math.isfinite(inputs.dt_ms)
|
| 101 |
+
or not 0 < inputs.dt_ms <= 1
|
| 102 |
+
):
|
| 103 |
+
raise ValueError("dt_ms must be finite in (0,1]")
|
| 104 |
+
edges = len(inputs.sources)
|
| 105 |
+
if edges > 65536 or len(inputs.offsets) != n + 1:
|
| 106 |
+
raise ValueError("invalid incoming CSR shape")
|
| 107 |
+
for values, upper in (
|
| 108 |
+
(inputs.offsets, edges),
|
| 109 |
+
(inputs.sources, n - 1),
|
| 110 |
+
(inputs.delays, 64),
|
| 111 |
+
):
|
| 112 |
+
if any(not _bounded_integer(x, 0, upper) for x in values):
|
| 113 |
+
raise ValueError("invalid CSR integer/index/delay")
|
| 114 |
+
if (
|
| 115 |
+
inputs.offsets[0] != 0
|
| 116 |
+
or inputs.offsets[-1] != edges
|
| 117 |
+
or any(a > b for a, b in zip(inputs.offsets, inputs.offsets[1:]))
|
| 118 |
+
):
|
| 119 |
+
raise ValueError("incoming CSR offsets must be monotone and span all edges")
|
| 120 |
+
if len(inputs.delays) != edges or any(x < 1 for x in inputs.delays):
|
| 121 |
+
raise ValueError("each edge requires a delay in [1,64]")
|
| 122 |
+
for name, numbers, length in (
|
| 123 |
+
("weights", inputs.weights, edges),
|
| 124 |
+
("external", inputs.external, n * ticks),
|
| 125 |
+
("voltage", inputs.voltage, n),
|
| 126 |
+
("adaptation", inputs.adaptation, n),
|
| 127 |
+
):
|
| 128 |
+
if len(numbers) != length or any(not _bounded_number(x) for x in numbers):
|
| 129 |
+
raise ValueError(
|
| 130 |
+
f"{name} has an invalid shape or non-finite/out-of-range values"
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def cpu_recurrent_reference(inputs: RecurrentInputs) -> dict[str, object]:
|
| 135 |
+
"""Use the existing Python neuron model as an independent scalar oracle."""
|
| 136 |
+
validate_recurrent_inputs(inputs)
|
| 137 |
+
profile = cuda_reference_model_profile(inputs.model)
|
| 138 |
+
states = [{"v": v, "w": w} for v, w in zip(inputs.voltage, inputs.adaptation)]
|
| 139 |
+
n = inputs.n_neurons
|
| 140 |
+
spikes: list[int] = []
|
| 141 |
+
voltage: list[float] = []
|
| 142 |
+
adaptation: list[float] = []
|
| 143 |
+
synapses = (
|
| 144 |
+
SynapseState(inputs, inputs.synapses) if inputs.synapses is not None else None
|
| 145 |
+
)
|
| 146 |
+
for tick in range(inputs.ticks):
|
| 147 |
+
for neuron in range(n):
|
| 148 |
+
current = inputs.external[tick * n + neuron]
|
| 149 |
+
for edge in range(inputs.offsets[neuron], inputs.offsets[neuron + 1]):
|
| 150 |
+
previous = tick - inputs.delays[edge]
|
| 151 |
+
if synapses is not None:
|
| 152 |
+
current += synapses.edge_current(tick, edge)
|
| 153 |
+
elif previous >= 0:
|
| 154 |
+
current += (
|
| 155 |
+
inputs.weights[edge]
|
| 156 |
+
* spikes[previous * n + inputs.sources[edge]]
|
| 157 |
+
)
|
| 158 |
+
spike = step_reference(profile, states[neuron], current, inputs.dt_ms)
|
| 159 |
+
spikes.append(int(spike))
|
| 160 |
+
voltage.append(float(states[neuron]["v"]))
|
| 161 |
+
adaptation.append(float(states[neuron]["w"]))
|
| 162 |
+
if synapses is not None:
|
| 163 |
+
synapses.update(
|
| 164 |
+
tick, spikes[tick * n : (tick + 1) * n], inputs.rewards[tick]
|
| 165 |
+
)
|
| 166 |
+
result: dict[str, object] = {
|
| 167 |
+
"voltage": voltage,
|
| 168 |
+
"adaptation": adaptation,
|
| 169 |
+
"spikes": spikes,
|
| 170 |
+
}
|
| 171 |
+
if synapses is not None:
|
| 172 |
+
result.update(
|
| 173 |
+
weights=synapses.weights,
|
| 174 |
+
eligibility=synapses.eligibility,
|
| 175 |
+
available=synapses.available,
|
| 176 |
+
)
|
| 177 |
+
return result
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def execute_recurrent(
|
| 181 |
+
inputs: RecurrentInputs,
|
| 182 |
+
*,
|
| 183 |
+
block_size: int = 64,
|
| 184 |
+
target_sm: str = "sm_86",
|
| 185 |
+
output_dir: Path | None = None,
|
| 186 |
+
) -> dict[str, object]:
|
| 187 |
+
validate_recurrent_inputs(inputs)
|
| 188 |
+
validate_cuda_block_size(block_size)
|
| 189 |
+
if output_dir is None:
|
| 190 |
+
with tempfile.TemporaryDirectory(prefix="mhrn-recurrent-") as directory:
|
| 191 |
+
return execute_recurrent(
|
| 192 |
+
inputs,
|
| 193 |
+
block_size=block_size,
|
| 194 |
+
target_sm=target_sm,
|
| 195 |
+
output_dir=Path(directory),
|
| 196 |
+
)
|
| 197 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 198 |
+
source = Path(__file__).with_name("kernel.cu").read_text()
|
| 199 |
+
if inputs.synapses is not None:
|
| 200 |
+
source = "#define PAN_PLASTIC\n" + source
|
| 201 |
+
ptx = compile_cuda_source(source, target_sm=target_sm)
|
| 202 |
+
artifact = output_dir / "recurrent.ptx"
|
| 203 |
+
artifact.write_text(ptx, encoding="utf-8")
|
| 204 |
+
driver = CudaDriver()
|
| 205 |
+
loaded = driver.load_cubin(artifact, kernel_name="pan_recurrent_kernel")
|
| 206 |
+
allocations: list[DeviceAllocation] = []
|
| 207 |
+
try:
|
| 208 |
+
n, count = inputs.n_neurons, inputs.n_neurons * inputs.ticks
|
| 209 |
+
ring_size = max(inputs.delays, default=1) + 1
|
| 210 |
+
params = cuda_reference_model_profile(inputs.model).parameters
|
| 211 |
+
parameter_row = [
|
| 212 |
+
params.get(
|
| 213 |
+
name, 1.0 if name in {"resistance", "tau_w_ms", "delta_t"} else 0.0
|
| 214 |
+
)
|
| 215 |
+
for name in PARAMETER_NAMES
|
| 216 |
+
]
|
| 217 |
+
uint, double = ctypes.c_uint32, ctypes.c_double
|
| 218 |
+
host: dict[str, Any] = {}
|
| 219 |
+
for name, kind, values in (
|
| 220 |
+
("offsets", uint, inputs.offsets),
|
| 221 |
+
("sources", uint, inputs.sources),
|
| 222 |
+
("delays", uint, inputs.delays),
|
| 223 |
+
("weights", double, inputs.weights),
|
| 224 |
+
("external", double, inputs.external),
|
| 225 |
+
("parameters", double, parameter_row * n),
|
| 226 |
+
("voltage", double, inputs.voltage),
|
| 227 |
+
("adaptation", double, inputs.adaptation),
|
| 228 |
+
("ring", uint, [0] * (ring_size * n)),
|
| 229 |
+
("voltage_history", double, [0.0] * count),
|
| 230 |
+
("adaptation_history", double, [0.0] * count),
|
| 231 |
+
("spikes", uint, [0] * count),
|
| 232 |
+
):
|
| 233 |
+
# CUDA does not accept a zero-byte allocation for an empty graph.
|
| 234 |
+
array_type: Any = kind * max(1, len(values))
|
| 235 |
+
host[name] = array_type(*values)
|
| 236 |
+
if inputs.synapses is not None:
|
| 237 |
+
edges = len(inputs.sources)
|
| 238 |
+
host.update(
|
| 239 |
+
synapse_config=(double * 11)(*inputs.synapses.parameters()),
|
| 240 |
+
rewards=(double * inputs.ticks)(*inputs.rewards),
|
| 241 |
+
eligibility=(double * edges)(),
|
| 242 |
+
available=(double * edges)(*([1.0] * edges)),
|
| 243 |
+
last_event=(ctypes.c_int32 * edges)(*([-10_000_000] * edges)),
|
| 244 |
+
last_spike=(ctypes.c_int32 * n)(*([-10_000_000] * n)),
|
| 245 |
+
emitted=(double * (ring_size * edges))(),
|
| 246 |
+
)
|
| 247 |
+
device: dict[str, DeviceAllocation] = {}
|
| 248 |
+
for name, buffer in host.items():
|
| 249 |
+
allocation = driver.alloc_device(ctypes.sizeof(buffer))
|
| 250 |
+
allocations.append(allocation)
|
| 251 |
+
device[name] = allocation
|
| 252 |
+
driver.copy_host_to_device(allocation, buffer)
|
| 253 |
+
arguments: list[object] = [
|
| 254 |
+
uint(n),
|
| 255 |
+
uint(inputs.ticks),
|
| 256 |
+
uint(ring_size),
|
| 257 |
+
uint(inputs.model != "lif"),
|
| 258 |
+
double(inputs.dt_ms),
|
| 259 |
+
]
|
| 260 |
+
arguments.extend(ctypes.c_uint64(device[name].ptr) for name in host)
|
| 261 |
+
preflight = driver.launch_cooperative(
|
| 262 |
+
loaded, n_neurons=n, block_size=block_size, arguments=arguments
|
| 263 |
+
)
|
| 264 |
+
driver.synchronize()
|
| 265 |
+
outputs: dict[str, object] = {}
|
| 266 |
+
for name, key in (
|
| 267 |
+
("voltage_history", "voltage"),
|
| 268 |
+
("adaptation_history", "adaptation"),
|
| 269 |
+
("spikes", "spikes"),
|
| 270 |
+
):
|
| 271 |
+
driver.copy_device_to_host(host[name], device[name])
|
| 272 |
+
outputs[key] = list(host[name])
|
| 273 |
+
if inputs.synapses is not None:
|
| 274 |
+
for name in ("weights", "eligibility", "available"):
|
| 275 |
+
driver.copy_device_to_host(host[name], device[name])
|
| 276 |
+
outputs[name] = list(host[name])
|
| 277 |
+
return {
|
| 278 |
+
"classification": (
|
| 279 |
+
"MHRN_CUDA15_PLASTIC_REFERENCE"
|
| 280 |
+
if inputs.synapses
|
| 281 |
+
else "MHRN_CUDA14_RECURRENT_REFERENCE"
|
| 282 |
+
),
|
| 283 |
+
"scientific_evidence": False,
|
| 284 |
+
"execution_status": "GPU_KERNEL_EXECUTED",
|
| 285 |
+
"state_dtype": "float64",
|
| 286 |
+
"comparison_scope": (
|
| 287 |
+
"MEMBRANE_AND_FROZEN_REWARD_PLASTICITY"
|
| 288 |
+
if inputs.synapses
|
| 289 |
+
else "STATIC_SYNAPSES_AND_MEMBRANE_ONLY"
|
| 290 |
+
),
|
| 291 |
+
"full_pan_backend": False,
|
| 292 |
+
"preflight": preflight.to_mapping(),
|
| 293 |
+
"ticks": inputs.ticks,
|
| 294 |
+
"outputs": outputs,
|
| 295 |
+
}
|
| 296 |
+
finally:
|
| 297 |
+
try:
|
| 298 |
+
for allocation in reversed(allocations):
|
| 299 |
+
try:
|
| 300 |
+
driver.free_device(allocation)
|
| 301 |
+
except CudaDriverError:
|
| 302 |
+
pass
|
| 303 |
+
finally:
|
| 304 |
+
driver.unload(loaded)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def recurrent_parity(
|
| 308 |
+
reference: dict[str, Any], candidate: dict[str, Any], *, tolerance: float = 1e-4
|
| 309 |
+
) -> dict[str, object]:
|
| 310 |
+
if isinstance(tolerance, bool) or not math.isfinite(tolerance) or tolerance < 0:
|
| 311 |
+
raise ValueError("tolerance must be finite and non-negative")
|
| 312 |
+
try:
|
| 313 |
+
names = ["voltage", "adaptation"]
|
| 314 |
+
if any(
|
| 315 |
+
name in reference or name in candidate
|
| 316 |
+
for name in ("weights", "eligibility", "available")
|
| 317 |
+
):
|
| 318 |
+
names.extend(("weights", "eligibility", "available"))
|
| 319 |
+
errors = {
|
| 320 |
+
name: max_abs_error(reference.get(name, []), candidate.get(name, []))
|
| 321 |
+
for name in names
|
| 322 |
+
}
|
| 323 |
+
except (ValueError, TypeError, OverflowError) as exc:
|
| 324 |
+
return {
|
| 325 |
+
"passed": False,
|
| 326 |
+
"failure_reason": str(exc),
|
| 327 |
+
"scientific_evidence": False,
|
| 328 |
+
}
|
| 329 |
+
spikes = reference.get("spikes", [])
|
| 330 |
+
other = candidate.get("spikes", [])
|
| 331 |
+
exact = (
|
| 332 |
+
bool(spikes)
|
| 333 |
+
and len(spikes)
|
| 334 |
+
== len(other)
|
| 335 |
+
== len(reference["voltage"])
|
| 336 |
+
== len(reference["adaptation"])
|
| 337 |
+
and all(a in (0, 1) and b in (0, 1) and a == b for a, b in zip(spikes, other))
|
| 338 |
+
)
|
| 339 |
+
return {
|
| 340 |
+
"passed": exact
|
| 341 |
+
and all(math.isfinite(x) and x <= tolerance for x in errors.values()),
|
| 342 |
+
"D1_spikes_exact": exact,
|
| 343 |
+
"D2_max_abs_error": errors,
|
| 344 |
+
"scientific_evidence": False,
|
| 345 |
+
"scope": (
|
| 346 |
+
"MEMBRANE_AND_FROZEN_REWARD_PLASTICITY"
|
| 347 |
+
if "weights" in names
|
| 348 |
+
else "MEMBRANE_AND_STATIC_SYNAPSES"
|
| 349 |
+
),
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def recurrent_fixture(
|
| 354 |
+
*, n_neurons: int = 129, ticks: int = 100, model: str = "pan_adex_5d"
|
| 355 |
+
) -> RecurrentInputs:
|
| 356 |
+
"""Nontrivial cross-block ring graph used by CLI, HTTP and hardware checks."""
|
| 357 |
+
if not _bounded_integer(n_neurons, 1, 4096):
|
| 358 |
+
raise ValueError("n_neurons must be in [1,4096]")
|
| 359 |
+
if not _bounded_integer(ticks, 1, 2000) or n_neurons * ticks > 4_000_000:
|
| 360 |
+
raise ValueError("ticks or history exceeds diagnostic capacity")
|
| 361 |
+
n = n_neurons
|
| 362 |
+
inputs = RecurrentInputs(
|
| 363 |
+
n,
|
| 364 |
+
ticks,
|
| 365 |
+
tuple(range(n + 1)),
|
| 366 |
+
tuple((i - 65) % n for i in range(n)),
|
| 367 |
+
tuple(1 + i % 7 for i in range(n)),
|
| 368 |
+
(2.0,) * n,
|
| 369 |
+
tuple(40.0 + (i % 5) * 2 for _ in range(ticks) for i in range(n)),
|
| 370 |
+
(-65.0,) * n,
|
| 371 |
+
(0.0,) * n,
|
| 372 |
+
model=model,
|
| 373 |
+
)
|
| 374 |
+
validate_recurrent_inputs(inputs)
|
| 375 |
+
return inputs
|
src/acceleration/cuda/recurrent/config.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Frozen model profiles for the bounded CUDA recurrent reference.
|
| 2 |
+
|
| 3 |
+
These profiles preserve the hardware-tested CUDA-1.4/1.5 membrane semantics
|
| 4 |
+
without importing the exploratory Playground registry. They are engineering
|
| 5 |
+
compatibility profiles, not a new scientific neuron-model registry.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import math
|
| 11 |
+
from collections.abc import Mapping
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
from types import MappingProxyType
|
| 14 |
+
from typing import Any, Final
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass(frozen=True, slots=True)
|
| 18 |
+
class CudaReferenceModelProfile:
|
| 19 |
+
name: str
|
| 20 |
+
parameters: Mapping[str, float]
|
| 21 |
+
family: str
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
_PROFILES: Final[dict[str, CudaReferenceModelProfile]] = {
|
| 25 |
+
"lif": CudaReferenceModelProfile(
|
| 26 |
+
"lif",
|
| 27 |
+
MappingProxyType(
|
| 28 |
+
{
|
| 29 |
+
"v_rest": -65.0,
|
| 30 |
+
"threshold": -50.0,
|
| 31 |
+
"reset": -65.0,
|
| 32 |
+
"tau_m_ms": 20.0,
|
| 33 |
+
"resistance": 1.0,
|
| 34 |
+
}
|
| 35 |
+
),
|
| 36 |
+
"bounded-lif-reference",
|
| 37 |
+
),
|
| 38 |
+
"adex": CudaReferenceModelProfile(
|
| 39 |
+
"adex",
|
| 40 |
+
MappingProxyType(
|
| 41 |
+
{
|
| 42 |
+
"v_rest": -65.0,
|
| 43 |
+
"v_t": -50.0,
|
| 44 |
+
"delta_t": 2.0,
|
| 45 |
+
"tau_m_ms": 20.0,
|
| 46 |
+
"tau_w_ms": 120.0,
|
| 47 |
+
"a": 0.02,
|
| 48 |
+
"b": 0.5,
|
| 49 |
+
"threshold": 20.0,
|
| 50 |
+
"reset": -58.0,
|
| 51 |
+
}
|
| 52 |
+
),
|
| 53 |
+
"bounded-adex-reference",
|
| 54 |
+
),
|
| 55 |
+
"pan_adex_5d": CudaReferenceModelProfile(
|
| 56 |
+
"pan_adex_5d",
|
| 57 |
+
MappingProxyType(
|
| 58 |
+
{
|
| 59 |
+
"v_rest": -65.0,
|
| 60 |
+
"v_t": -55.0,
|
| 61 |
+
"delta_t": 2.0,
|
| 62 |
+
"tau_m_ms": 20.0,
|
| 63 |
+
"tau_w_ms": 120.0,
|
| 64 |
+
"a": 0.02,
|
| 65 |
+
"b": 0.5,
|
| 66 |
+
"threshold": -20.0,
|
| 67 |
+
"reset": -60.0,
|
| 68 |
+
}
|
| 69 |
+
),
|
| 70 |
+
"bounded-pan-adex-membrane-reference",
|
| 71 |
+
),
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
SUPPORTED_MODELS: Final[frozenset[str]] = frozenset(_PROFILES)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def cuda_reference_model_profile(name: str) -> CudaReferenceModelProfile:
|
| 78 |
+
try:
|
| 79 |
+
return _PROFILES[name]
|
| 80 |
+
except KeyError as exc:
|
| 81 |
+
raise ValueError(f"unsupported recurrent membrane model: {name}") from exc
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def step_reference(
|
| 85 |
+
profile: CudaReferenceModelProfile,
|
| 86 |
+
state: dict[str, Any],
|
| 87 |
+
current: float,
|
| 88 |
+
dt_ms: float,
|
| 89 |
+
) -> bool:
|
| 90 |
+
params = profile.parameters
|
| 91 |
+
v = float(state["v"])
|
| 92 |
+
if profile.name == "lif":
|
| 93 |
+
tau = max(params["tau_m_ms"], 1e-6)
|
| 94 |
+
v += dt_ms * (
|
| 95 |
+
(params["v_rest"] - v + params.get("resistance", 1.0) * current) / tau
|
| 96 |
+
)
|
| 97 |
+
spiked = v >= params["threshold"]
|
| 98 |
+
state["v"] = params["reset"] if spiked else v
|
| 99 |
+
state["w"] = float(state.get("w", 0.0))
|
| 100 |
+
return spiked
|
| 101 |
+
|
| 102 |
+
w = float(state.get("w", 0.0))
|
| 103 |
+
exponent = min(20.0, (v - params["v_t"]) / params["delta_t"])
|
| 104 |
+
dv = (
|
| 105 |
+
-(v - params["v_rest"]) + params["delta_t"] * math.exp(exponent) - w + current
|
| 106 |
+
) / params["tau_m_ms"]
|
| 107 |
+
dw = (params["a"] * (v - params["v_rest"]) - w) / params["tau_w_ms"]
|
| 108 |
+
v += dt_ms * dv
|
| 109 |
+
w += dt_ms * dw
|
| 110 |
+
spiked = v >= params["threshold"]
|
| 111 |
+
if spiked:
|
| 112 |
+
v = params["reset"]
|
| 113 |
+
w += params["b"]
|
| 114 |
+
state["v"] = v
|
| 115 |
+
state["w"] = w
|
| 116 |
+
return spiked
|
src/acceleration/cuda/recurrent/kernel.cu
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include <cooperative_groups.h>
|
| 2 |
+
|
| 3 |
+
#ifdef PAN_PLASTIC
|
| 4 |
+
__device__ double release_uniform(unsigned seed, unsigned tick, unsigned edge) {
|
| 5 |
+
unsigned bits = seed ^ (tick * 0x9E3779B9u) ^ (edge * 0x85EBCA6Bu);
|
| 6 |
+
bits ^= bits >> 16; bits *= 0x7FEB352Du;
|
| 7 |
+
bits ^= bits >> 15; bits *= 0x846CA68Bu; bits ^= bits >> 16;
|
| 8 |
+
return (bits >> 8) / 16777216.0;
|
| 9 |
+
}
|
| 10 |
+
#endif
|
| 11 |
+
|
| 12 |
+
// Deterministic incoming-edge gather: no floating point atomic accumulation.
|
| 13 |
+
// All blocks participate in the barrier, including partial-block inactive lanes.
|
| 14 |
+
extern "C" __global__ void pan_recurrent_kernel(
|
| 15 |
+
const unsigned n, const unsigned ticks, const unsigned ring_size,
|
| 16 |
+
const unsigned model, const double dt,
|
| 17 |
+
const unsigned* offsets, const unsigned* sources,
|
| 18 |
+
const unsigned* delays, double* weights,
|
| 19 |
+
const double* external, const double* parameters,
|
| 20 |
+
double* voltage, double* adaptation, unsigned* ring,
|
| 21 |
+
double* voltage_history, double* adaptation_history, unsigned* spikes
|
| 22 |
+
#ifdef PAN_PLASTIC
|
| 23 |
+
, const double* config, const double* rewards, double* eligibility,
|
| 24 |
+
double* available, int* last_event, int* last_spike, double* emitted
|
| 25 |
+
#endif
|
| 26 |
+
) {
|
| 27 |
+
auto grid = cooperative_groups::this_grid();
|
| 28 |
+
const unsigned neuron = blockIdx.x * blockDim.x + threadIdx.x;
|
| 29 |
+
for (unsigned tick = 0; tick < ticks; ++tick) {
|
| 30 |
+
if (neuron < n) {
|
| 31 |
+
double current = external[tick * n + neuron];
|
| 32 |
+
for (unsigned edge = offsets[neuron]; edge < offsets[neuron + 1]; ++edge) {
|
| 33 |
+
if (tick >= delays[edge]) {
|
| 34 |
+
const unsigned slot = (tick - delays[edge]) % ring_size;
|
| 35 |
+
#ifdef PAN_PLASTIC
|
| 36 |
+
current += emitted[slot * offsets[n] + edge];
|
| 37 |
+
#else
|
| 38 |
+
current += weights[edge] * ring[slot * n + sources[edge]];
|
| 39 |
+
#endif
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
const double* p = parameters + neuron * 10;
|
| 43 |
+
double v = voltage[neuron];
|
| 44 |
+
double w = adaptation[neuron];
|
| 45 |
+
if (model == 0) {
|
| 46 |
+
v += dt * ((p[0] - v + p[9] * current) / p[3]);
|
| 47 |
+
} else {
|
| 48 |
+
const double exponent = fmin(20.0, (v - p[1]) / p[2]);
|
| 49 |
+
const double dv = (-(v - p[0]) + p[2] * exp(exponent) - w + current) / p[3];
|
| 50 |
+
const double dw = (p[5] * (v - p[0]) - w) / p[4];
|
| 51 |
+
v += dt * dv;
|
| 52 |
+
w += dt * dw;
|
| 53 |
+
}
|
| 54 |
+
const unsigned spike = v >= p[7];
|
| 55 |
+
if (spike) {
|
| 56 |
+
v = p[8];
|
| 57 |
+
if (model != 0) w += p[6];
|
| 58 |
+
}
|
| 59 |
+
voltage[neuron] = v;
|
| 60 |
+
adaptation[neuron] = w;
|
| 61 |
+
voltage_history[tick * n + neuron] = v;
|
| 62 |
+
adaptation_history[tick * n + neuron] = w;
|
| 63 |
+
spikes[tick * n + neuron] = spike;
|
| 64 |
+
ring[(tick % ring_size) * n + neuron] = spike;
|
| 65 |
+
}
|
| 66 |
+
grid.sync();
|
| 67 |
+
#ifdef PAN_PLASTIC
|
| 68 |
+
if (neuron < n) {
|
| 69 |
+
const unsigned post = spikes[tick * n + neuron];
|
| 70 |
+
for (unsigned edge = offsets[neuron]; edge < offsets[neuron + 1]; ++edge) {
|
| 71 |
+
const unsigned source = sources[edge];
|
| 72 |
+
const unsigned pre = spikes[tick * n + source];
|
| 73 |
+
double weight = weights[edge];
|
| 74 |
+
double trace = eligibility[edge] * exp(-dt / config[7]);
|
| 75 |
+
const int since_pre = (int)tick - last_spike[source];
|
| 76 |
+
const int since_post = (int)tick - last_spike[neuron];
|
| 77 |
+
for (unsigned phase = 0; phase < 2; ++phase) {
|
| 78 |
+
const bool pre_phase = source <= neuron ? phase == 0 : phase == 1;
|
| 79 |
+
if (pre_phase && pre) {
|
| 80 |
+
if (config[0] && since_post > 0 && since_post <= 20)
|
| 81 |
+
weight = fmax(0.0, weight - 0.08);
|
| 82 |
+
trace -= 0.5;
|
| 83 |
+
}
|
| 84 |
+
if (!pre_phase && post) {
|
| 85 |
+
if (config[0] && since_pre > 0 && since_pre <= 20)
|
| 86 |
+
weight = fmin(100.0, weight + 0.1);
|
| 87 |
+
trace += 1.0;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
if (pre || post) last_event[edge] = tick;
|
| 91 |
+
if (config[2] && (int)tick - last_event[edge] <= config[8])
|
| 92 |
+
weight = fmin(100.0, fmax(0.0, weight + config[4] * config[9] * config[10] * trace * rewards[tick]));
|
| 93 |
+
double amplitude = pre ? weight : 0.0;
|
| 94 |
+
double resource = available[edge];
|
| 95 |
+
if (config[1]) {
|
| 96 |
+
if (pre) {
|
| 97 |
+
amplitude *= release_uniform((unsigned)config[3], tick, edge) < fmin(0.95, 0.25 + 0.7 * resource);
|
| 98 |
+
resource = fmax(0.1, resource * 0.72);
|
| 99 |
+
}
|
| 100 |
+
resource = fmin(1.0, resource + 0.025);
|
| 101 |
+
}
|
| 102 |
+
available[edge] = resource;
|
| 103 |
+
emitted[(tick % ring_size) * offsets[n] + edge] = amplitude;
|
| 104 |
+
weights[edge] = fmin(config[6], fmax(0.0, weight * (1.0 - config[5])));
|
| 105 |
+
eligibility[edge] = trace;
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
grid.sync();
|
| 109 |
+
if (neuron < n && spikes[tick * n + neuron]) last_spike[neuron] = tick;
|
| 110 |
+
grid.sync();
|
| 111 |
+
#endif
|
| 112 |
+
}
|
| 113 |
+
}
|
src/acceleration/cuda/recurrent/reference.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CPU scalar reference backend for canonical Wave-4 parity."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Mapping, Sequence
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from src.runtime.backend import BackendCapabilities, BackendState, RunResult, StepResult
|
| 9 |
+
from src.verification.parity import config_fingerprint, execution_fingerprint
|
| 10 |
+
|
| 11 |
+
from .backend import cpu_recurrent_reference
|
| 12 |
+
from .state import (
|
| 13 |
+
canonical_int,
|
| 14 |
+
prefix_inputs,
|
| 15 |
+
recurrent_inputs_from_mapping,
|
| 16 |
+
recurrent_inputs_to_mapping,
|
| 17 |
+
set_external_tick_config,
|
| 18 |
+
stable_digest,
|
| 19 |
+
step_payload,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class CPUReferenceBackend:
|
| 24 |
+
backend_name = "cpu-reference"
|
| 25 |
+
backend_version = "wave4-scalar-reference-v1"
|
| 26 |
+
|
| 27 |
+
def __init__(self) -> None:
|
| 28 |
+
self._config: dict[str, object] | None = None
|
| 29 |
+
self._seed = 0
|
| 30 |
+
self._tick = 0
|
| 31 |
+
|
| 32 |
+
def initialize(self, config: Mapping[str, object], seed: int) -> None:
|
| 33 |
+
inputs = recurrent_inputs_from_mapping(config, seed=seed)
|
| 34 |
+
self._config = recurrent_inputs_to_mapping(inputs)
|
| 35 |
+
self._seed = seed
|
| 36 |
+
self._tick = 0
|
| 37 |
+
|
| 38 |
+
def _require_config(self) -> dict[str, object]:
|
| 39 |
+
if self._config is None:
|
| 40 |
+
raise RuntimeError("backend is not initialized")
|
| 41 |
+
return self._config
|
| 42 |
+
|
| 43 |
+
def _execute_prefix(self, total_ticks: int) -> Mapping[str, object]:
|
| 44 |
+
inputs = recurrent_inputs_from_mapping(self._require_config(), seed=self._seed)
|
| 45 |
+
return cpu_recurrent_reference(prefix_inputs(inputs, total_ticks))
|
| 46 |
+
|
| 47 |
+
def _step_from_outputs(
|
| 48 |
+
self, outputs: Mapping[str, object], tick: int
|
| 49 |
+
) -> StepResult:
|
| 50 |
+
config = self._require_config()
|
| 51 |
+
payload = step_payload(
|
| 52 |
+
outputs,
|
| 53 |
+
tick=tick,
|
| 54 |
+
n_neurons=canonical_int(config["n_neurons"], field="n_neurons"),
|
| 55 |
+
)
|
| 56 |
+
spikes_raw = payload["spikes"]
|
| 57 |
+
if not isinstance(spikes_raw, list):
|
| 58 |
+
raise ValueError("spike payload must be a list")
|
| 59 |
+
spikes = tuple(
|
| 60 |
+
index for index, value in enumerate(spikes_raw) if int(value) == 1
|
| 61 |
+
)
|
| 62 |
+
return StepResult(
|
| 63 |
+
tick=tick + 1,
|
| 64 |
+
spikes=spikes,
|
| 65 |
+
state_digest=stable_digest(payload),
|
| 66 |
+
metrics={
|
| 67 |
+
"voltage": payload["voltage"],
|
| 68 |
+
"adaptation": payload["adaptation"],
|
| 69 |
+
"spike_count": len(spikes),
|
| 70 |
+
},
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
def set_external_tick(self, tick: int, currents: Sequence[float]) -> None:
|
| 74 |
+
if tick != self._tick:
|
| 75 |
+
raise ValueError("live external input tick must equal continuation cursor")
|
| 76 |
+
set_external_tick_config(self._require_config(), tick=tick, currents=currents)
|
| 77 |
+
|
| 78 |
+
def step(self, tick: int) -> StepResult:
|
| 79 |
+
if tick != self._tick:
|
| 80 |
+
raise ValueError("tick must equal the backend continuation cursor")
|
| 81 |
+
config = self._require_config()
|
| 82 |
+
if tick >= canonical_int(config["ticks"], field="ticks"):
|
| 83 |
+
raise ValueError("configured tick limit reached")
|
| 84 |
+
outputs = self._execute_prefix(tick + 1)
|
| 85 |
+
self._tick = tick + 1
|
| 86 |
+
return self._step_from_outputs(outputs, tick)
|
| 87 |
+
|
| 88 |
+
def run(self, ticks: int) -> RunResult:
|
| 89 |
+
if type(ticks) is not int or ticks < 1:
|
| 90 |
+
raise ValueError("ticks must be a positive int")
|
| 91 |
+
config = self._require_config()
|
| 92 |
+
total = self._tick + ticks
|
| 93 |
+
if total > canonical_int(config["ticks"], field="ticks"):
|
| 94 |
+
raise ValueError("requested run exceeds configured tick limit")
|
| 95 |
+
start = self._tick
|
| 96 |
+
outputs = self._execute_prefix(total)
|
| 97 |
+
steps = tuple(
|
| 98 |
+
self._step_from_outputs(outputs, tick) for tick in range(start, total)
|
| 99 |
+
)
|
| 100 |
+
self._tick = total
|
| 101 |
+
return RunResult(
|
| 102 |
+
ticks_requested=ticks,
|
| 103 |
+
steps=steps,
|
| 104 |
+
final_state=self.snapshot(),
|
| 105 |
+
execution_fingerprint=execution_fingerprint(
|
| 106 |
+
seed=self._seed,
|
| 107 |
+
config_hash=config_fingerprint(config),
|
| 108 |
+
backend_name=self.backend_name,
|
| 109 |
+
backend_version=self.backend_version,
|
| 110 |
+
ticks=total,
|
| 111 |
+
),
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
def snapshot(self) -> BackendState:
|
| 115 |
+
config = self._require_config()
|
| 116 |
+
payload: dict[str, Any] = {
|
| 117 |
+
"backend": self.backend_version,
|
| 118 |
+
"seed": self._seed,
|
| 119 |
+
"tick": self._tick,
|
| 120 |
+
"config": config,
|
| 121 |
+
"replay_required": True,
|
| 122 |
+
}
|
| 123 |
+
return BackendState(
|
| 124 |
+
tick=self._tick,
|
| 125 |
+
payload=payload,
|
| 126 |
+
state_digest=stable_digest(payload),
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
def restore(self, state: BackendState) -> None:
|
| 130 |
+
payload = dict(state.payload)
|
| 131 |
+
if payload.get("backend") != self.backend_version:
|
| 132 |
+
raise ValueError("checkpoint backend version mismatch")
|
| 133 |
+
raw_config = payload.get("config")
|
| 134 |
+
raw_seed = payload.get("seed")
|
| 135 |
+
if not isinstance(raw_config, Mapping) or type(raw_seed) is not int:
|
| 136 |
+
raise ValueError("checkpoint is missing canonical config/seed")
|
| 137 |
+
self.initialize(raw_config, raw_seed)
|
| 138 |
+
self._tick = state.tick
|
| 139 |
+
|
| 140 |
+
def capabilities(self) -> BackendCapabilities:
|
| 141 |
+
return BackendCapabilities(
|
| 142 |
+
supports_recurrent=True,
|
| 143 |
+
supports_plasticity=True,
|
| 144 |
+
supports_pan_hyperstate=False,
|
| 145 |
+
supports_structural_plasticity=False,
|
| 146 |
+
max_neurons=4096,
|
| 147 |
+
max_ticks=2000,
|
| 148 |
+
max_edges=65536,
|
| 149 |
+
deterministic=True,
|
| 150 |
+
plasticity_semantics="REFERENCE_MATCH_TARGET_DRAFT",
|
| 151 |
+
execution_mode="BOUNDED_REPLAY_REFERENCE",
|
| 152 |
+
supports_live_external_input=True,
|
| 153 |
+
)
|
src/acceleration/cuda/recurrent/state.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Serialization helpers for the bounded recurrent backend."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
import json
|
| 7 |
+
from collections.abc import Mapping, Sequence
|
| 8 |
+
from dataclasses import asdict, replace
|
| 9 |
+
|
| 10 |
+
from ..plasticity.reference import SynapseConfig
|
| 11 |
+
from .backend import RecurrentInputs, validate_recurrent_inputs
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def stable_digest(value: object) -> str:
|
| 15 |
+
encoded = json.dumps(
|
| 16 |
+
value,
|
| 17 |
+
sort_keys=True,
|
| 18 |
+
separators=(",", ":"),
|
| 19 |
+
ensure_ascii=False,
|
| 20 |
+
allow_nan=False,
|
| 21 |
+
).encode("utf-8")
|
| 22 |
+
return hashlib.sha256(encoded).hexdigest()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def canonical_int(value: object, *, field: str) -> int:
|
| 26 |
+
if isinstance(value, bool) or not isinstance(value, int):
|
| 27 |
+
raise ValueError(f"{field} must be an integer")
|
| 28 |
+
return value
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def canonical_float(value: object, *, field: str) -> float:
|
| 32 |
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
| 33 |
+
raise ValueError(f"{field} must be numeric")
|
| 34 |
+
return float(value)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def recurrent_inputs_to_mapping(inputs: RecurrentInputs) -> dict[str, object]:
|
| 38 |
+
result: dict[str, object] = {
|
| 39 |
+
"n_neurons": inputs.n_neurons,
|
| 40 |
+
"ticks": inputs.ticks,
|
| 41 |
+
"offsets": list(inputs.offsets),
|
| 42 |
+
"sources": list(inputs.sources),
|
| 43 |
+
"delays": list(inputs.delays),
|
| 44 |
+
"weights": list(inputs.weights),
|
| 45 |
+
"external": list(inputs.external),
|
| 46 |
+
"voltage": list(inputs.voltage),
|
| 47 |
+
"adaptation": list(inputs.adaptation),
|
| 48 |
+
"model": inputs.model,
|
| 49 |
+
"dt_ms": inputs.dt_ms,
|
| 50 |
+
"rewards": list(inputs.rewards),
|
| 51 |
+
}
|
| 52 |
+
result["synapses"] = (
|
| 53 |
+
asdict(inputs.synapses) if inputs.synapses is not None else None
|
| 54 |
+
)
|
| 55 |
+
return result
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _tuple_int(values: object, *, field: str) -> tuple[int, ...]:
|
| 59 |
+
if not isinstance(values, (list, tuple)):
|
| 60 |
+
raise ValueError(f"{field} must be a list/tuple")
|
| 61 |
+
return tuple(
|
| 62 |
+
canonical_int(value, field=f"{field}[{index}]")
|
| 63 |
+
for index, value in enumerate(values)
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _tuple_float(values: object, *, field: str) -> tuple[float, ...]:
|
| 68 |
+
if not isinstance(values, (list, tuple)):
|
| 69 |
+
raise ValueError(f"{field} must be a list/tuple")
|
| 70 |
+
return tuple(
|
| 71 |
+
canonical_float(value, field=f"{field}[{index}]")
|
| 72 |
+
for index, value in enumerate(values)
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def recurrent_inputs_from_mapping(
|
| 77 |
+
config: Mapping[str, object],
|
| 78 |
+
*,
|
| 79 |
+
seed: int,
|
| 80 |
+
) -> RecurrentInputs:
|
| 81 |
+
if type(seed) is not int or not 0 <= seed <= 0xFFFFFFFF:
|
| 82 |
+
raise ValueError("seed must be uint32")
|
| 83 |
+
raw_synapses = config.get("synapses")
|
| 84 |
+
synapses: SynapseConfig | None = None
|
| 85 |
+
if raw_synapses is not None:
|
| 86 |
+
if not isinstance(raw_synapses, Mapping):
|
| 87 |
+
raise ValueError("synapses must be a mapping or null")
|
| 88 |
+
supplied_seed = canonical_int(
|
| 89 |
+
raw_synapses.get("seed", seed), field="synapses.seed"
|
| 90 |
+
)
|
| 91 |
+
if supplied_seed != seed:
|
| 92 |
+
raise ValueError("backend seed must match synapse seed")
|
| 93 |
+
synapses = SynapseConfig(
|
| 94 |
+
stdp=bool(raw_synapses.get("stdp", True)),
|
| 95 |
+
stp=bool(raw_synapses.get("stp", True)),
|
| 96 |
+
reward_modulated=bool(raw_synapses.get("reward_modulated", True)),
|
| 97 |
+
seed=seed,
|
| 98 |
+
learning_rate=canonical_float(
|
| 99 |
+
raw_synapses.get("learning_rate", 0.2),
|
| 100 |
+
field="synapses.learning_rate",
|
| 101 |
+
),
|
| 102 |
+
weight_decay=canonical_float(
|
| 103 |
+
raw_synapses.get("weight_decay", 0.001),
|
| 104 |
+
field="synapses.weight_decay",
|
| 105 |
+
),
|
| 106 |
+
weight_max=canonical_float(
|
| 107 |
+
raw_synapses.get("weight_max", 20.0),
|
| 108 |
+
field="synapses.weight_max",
|
| 109 |
+
),
|
| 110 |
+
eligibility_tau=canonical_float(
|
| 111 |
+
raw_synapses.get("eligibility_tau", 100.0),
|
| 112 |
+
field="synapses.eligibility_tau",
|
| 113 |
+
),
|
| 114 |
+
credit_window=canonical_int(
|
| 115 |
+
raw_synapses.get("credit_window", 64),
|
| 116 |
+
field="synapses.credit_window",
|
| 117 |
+
),
|
| 118 |
+
td_lambda=canonical_float(
|
| 119 |
+
raw_synapses.get("td_lambda", 0.9),
|
| 120 |
+
field="synapses.td_lambda",
|
| 121 |
+
),
|
| 122 |
+
gamma=canonical_float(
|
| 123 |
+
raw_synapses.get("gamma", 0.99),
|
| 124 |
+
field="synapses.gamma",
|
| 125 |
+
),
|
| 126 |
+
)
|
| 127 |
+
try:
|
| 128 |
+
inputs = RecurrentInputs(
|
| 129 |
+
n_neurons=canonical_int(config["n_neurons"], field="n_neurons"),
|
| 130 |
+
ticks=canonical_int(config["ticks"], field="ticks"),
|
| 131 |
+
offsets=_tuple_int(config["offsets"], field="offsets"),
|
| 132 |
+
sources=_tuple_int(config["sources"], field="sources"),
|
| 133 |
+
delays=_tuple_int(config["delays"], field="delays"),
|
| 134 |
+
weights=_tuple_float(config["weights"], field="weights"),
|
| 135 |
+
external=_tuple_float(config["external"], field="external"),
|
| 136 |
+
voltage=_tuple_float(config["voltage"], field="voltage"),
|
| 137 |
+
adaptation=_tuple_float(config["adaptation"], field="adaptation"),
|
| 138 |
+
model=str(config.get("model", "lif")),
|
| 139 |
+
dt_ms=canonical_float(config.get("dt_ms", 1.0), field="dt_ms"),
|
| 140 |
+
synapses=synapses,
|
| 141 |
+
rewards=_tuple_float(config.get("rewards", ()), field="rewards"),
|
| 142 |
+
)
|
| 143 |
+
except KeyError as exc:
|
| 144 |
+
raise ValueError(f"missing recurrent config field: {exc.args[0]}") from exc
|
| 145 |
+
validate_recurrent_inputs(inputs)
|
| 146 |
+
return inputs
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def set_external_tick_config(
|
| 150 |
+
config: dict[str, object],
|
| 151 |
+
*,
|
| 152 |
+
tick: int,
|
| 153 |
+
currents: Sequence[float],
|
| 154 |
+
) -> None:
|
| 155 |
+
"""Replace exactly one external-current row in a canonical replay config."""
|
| 156 |
+
|
| 157 |
+
n = canonical_int(config.get("n_neurons"), field="n_neurons")
|
| 158 |
+
ticks = canonical_int(config.get("ticks"), field="ticks")
|
| 159 |
+
if type(tick) is not int or not 0 <= tick < ticks:
|
| 160 |
+
raise ValueError("tick is outside configured replay range")
|
| 161 |
+
if len(currents) != n:
|
| 162 |
+
raise ValueError("external-current row must match n_neurons")
|
| 163 |
+
raw_external = config.get("external")
|
| 164 |
+
if not isinstance(raw_external, (list, tuple)):
|
| 165 |
+
raise ValueError("external must be a list/tuple")
|
| 166 |
+
if len(raw_external) != n * ticks:
|
| 167 |
+
raise ValueError("external has invalid configured shape")
|
| 168 |
+
external = [
|
| 169 |
+
canonical_float(value, field=f"external[{index}]")
|
| 170 |
+
for index, value in enumerate(raw_external)
|
| 171 |
+
]
|
| 172 |
+
row = [
|
| 173 |
+
canonical_float(value, field=f"currents[{index}]")
|
| 174 |
+
for index, value in enumerate(currents)
|
| 175 |
+
]
|
| 176 |
+
start = tick * n
|
| 177 |
+
external[start : start + n] = row
|
| 178 |
+
config["external"] = external
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def prefix_inputs(inputs: RecurrentInputs, ticks: int) -> RecurrentInputs:
|
| 182 |
+
if not 1 <= ticks <= inputs.ticks:
|
| 183 |
+
raise ValueError("prefix ticks outside configured range")
|
| 184 |
+
n = inputs.n_neurons
|
| 185 |
+
return replace(
|
| 186 |
+
inputs,
|
| 187 |
+
ticks=ticks,
|
| 188 |
+
external=inputs.external[: ticks * n],
|
| 189 |
+
rewards=inputs.rewards[:ticks] if inputs.synapses is not None else (),
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def step_payload(
|
| 194 |
+
outputs: Mapping[str, object],
|
| 195 |
+
*,
|
| 196 |
+
tick: int,
|
| 197 |
+
n_neurons: int,
|
| 198 |
+
) -> dict[str, object]:
|
| 199 |
+
start, stop = tick * n_neurons, (tick + 1) * n_neurons
|
| 200 |
+
voltage_raw = outputs.get("voltage")
|
| 201 |
+
adaptation_raw = outputs.get("adaptation")
|
| 202 |
+
spikes_raw = outputs.get("spikes")
|
| 203 |
+
if not isinstance(voltage_raw, list):
|
| 204 |
+
raise ValueError("voltage output must be a list")
|
| 205 |
+
if not isinstance(adaptation_raw, list):
|
| 206 |
+
raise ValueError("adaptation output must be a list")
|
| 207 |
+
if not isinstance(spikes_raw, list):
|
| 208 |
+
raise ValueError("spikes output must be a list")
|
| 209 |
+
return {
|
| 210 |
+
"tick": tick,
|
| 211 |
+
"voltage": [
|
| 212 |
+
canonical_float(value, field="voltage output")
|
| 213 |
+
for value in voltage_raw[start:stop]
|
| 214 |
+
],
|
| 215 |
+
"adaptation": [
|
| 216 |
+
canonical_float(value, field="adaptation output")
|
| 217 |
+
for value in adaptation_raw[start:stop]
|
| 218 |
+
],
|
| 219 |
+
"spikes": [
|
| 220 |
+
canonical_int(value, field="spike output")
|
| 221 |
+
for value in spikes_raw[start:stop]
|
| 222 |
+
],
|
| 223 |
+
}
|
src/controller/runtime.py
CHANGED
|
@@ -23,7 +23,7 @@ Example:
|
|
| 23 |
Integration with dashboard:
|
| 24 |
>>> from src.dashboard.operator_bridge import OperatorBridge
|
| 25 |
>>> bridge = OperatorBridge(controller=controller)
|
| 26 |
-
>>> serve_dashboard(host="127.0.0.1", port=
|
| 27 |
"""
|
| 28 |
|
| 29 |
from __future__ import annotations
|
|
|
|
| 23 |
Integration with dashboard:
|
| 24 |
>>> from src.dashboard.operator_bridge import OperatorBridge
|
| 25 |
>>> bridge = OperatorBridge(controller=controller)
|
| 26 |
+
>>> serve_dashboard(host="127.0.0.1", port=8767, structural_bridge=bridge)
|
| 27 |
"""
|
| 28 |
|
| 29 |
from __future__ import annotations
|
src/dashboard/__init__.py
CHANGED
|
@@ -15,10 +15,10 @@ and accessed via a web browser.
|
|
| 15 |
|
| 16 |
Example:
|
| 17 |
>>> from src.dashboard import serve_dashboard
|
| 18 |
-
>>> serve_dashboard(host="127.0.0.1", port=
|
| 19 |
|
| 20 |
Or via command line:
|
| 21 |
-
>>> python -m src.dashboard --host 0.0.0.0 --port
|
| 22 |
"""
|
| 23 |
|
| 24 |
from __future__ import annotations
|
|
@@ -45,6 +45,7 @@ from .models import (
|
|
| 45 |
StructuralMetrics,
|
| 46 |
SystemMetrics,
|
| 47 |
)
|
|
|
|
| 48 |
from .operator_bridge import OperatorBridge
|
| 49 |
from .server import serve_dashboard
|
| 50 |
from .state import (
|
|
@@ -67,8 +68,8 @@ logger = logging.getLogger(__name__)
|
|
| 67 |
|
| 68 |
|
| 69 |
def create_default_dashboard(
|
| 70 |
-
host: str =
|
| 71 |
-
port: int =
|
| 72 |
snapshot_path: str | None = None,
|
| 73 |
docs_root: str | None = None,
|
| 74 |
state_store: DashboardStateStore | None = None,
|
|
@@ -81,7 +82,7 @@ def create_default_dashboard(
|
|
| 81 |
|
| 82 |
Args:
|
| 83 |
host: Host address to bind to (default: 127.0.0.1).
|
| 84 |
-
port: Port to bind to (default:
|
| 85 |
snapshot_path: Optional path to the default B5D snapshot.
|
| 86 |
docs_root: Optional path to the documentation root.
|
| 87 |
state_store: Optional custom state store.
|
|
|
|
| 15 |
|
| 16 |
Example:
|
| 17 |
>>> from src.dashboard import serve_dashboard
|
| 18 |
+
>>> serve_dashboard(host="127.0.0.1", port=8767)
|
| 19 |
|
| 20 |
Or via command line:
|
| 21 |
+
>>> python -m src.dashboard --host 0.0.0.0 --port 8767
|
| 22 |
"""
|
| 23 |
|
| 24 |
from __future__ import annotations
|
|
|
|
| 45 |
StructuralMetrics,
|
| 46 |
SystemMetrics,
|
| 47 |
)
|
| 48 |
+
from .network_settings import DASHBOARD_HOST, DASHBOARD_PORT
|
| 49 |
from .operator_bridge import OperatorBridge
|
| 50 |
from .server import serve_dashboard
|
| 51 |
from .state import (
|
|
|
|
| 68 |
|
| 69 |
|
| 70 |
def create_default_dashboard(
|
| 71 |
+
host: str = DASHBOARD_HOST,
|
| 72 |
+
port: int = DASHBOARD_PORT,
|
| 73 |
snapshot_path: str | None = None,
|
| 74 |
docs_root: str | None = None,
|
| 75 |
state_store: DashboardStateStore | None = None,
|
|
|
|
| 82 |
|
| 83 |
Args:
|
| 84 |
host: Host address to bind to (default: 127.0.0.1).
|
| 85 |
+
port: Port to bind to (default: 8767).
|
| 86 |
snapshot_path: Optional path to the default B5D snapshot.
|
| 87 |
docs_root: Optional path to the documentation root.
|
| 88 |
state_store: Optional custom state store.
|
src/dashboard/experiment_evaluation.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Controlled post-hoc scientific interpretation artifacts for experiments."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import re
|
| 7 |
+
from datetime import datetime, timezone
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any, cast
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ExperimentEvaluationError(ValueError):
|
| 13 |
+
"""Raised when a post-hoc evaluation cannot be safely recorded."""
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
_EXPERIMENT_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
| 17 |
+
_ANSWERS = frozenset({"supported", "refuted", "mixed", "inconclusive", "not_assessed"})
|
| 18 |
+
_TEXT_LIMITS = {
|
| 19 |
+
"evaluator": 200,
|
| 20 |
+
"evaluation": 30_000,
|
| 21 |
+
"observations": 20_000,
|
| 22 |
+
"limitations": 20_000,
|
| 23 |
+
"follow_up": 20_000,
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _text(payload: dict[str, object], key: str, *, required: bool = False) -> str:
|
| 28 |
+
value = payload.get(key, "")
|
| 29 |
+
if not isinstance(value, str):
|
| 30 |
+
raise ExperimentEvaluationError(f"{key} must be text")
|
| 31 |
+
result = value.strip()
|
| 32 |
+
if required and not result:
|
| 33 |
+
raise ExperimentEvaluationError(f"{key} is required")
|
| 34 |
+
limit = _TEXT_LIMITS.get(key)
|
| 35 |
+
if limit is not None and len(result) > limit:
|
| 36 |
+
raise ExperimentEvaluationError(f"{key} exceeds {limit} characters")
|
| 37 |
+
return result
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _experiment_dir(research_root: Path, experiment_id: str) -> Path:
|
| 41 |
+
if not _EXPERIMENT_ID.fullmatch(experiment_id):
|
| 42 |
+
raise ExperimentEvaluationError("invalid experiment_id")
|
| 43 |
+
directory = research_root / "experiments" / experiment_id
|
| 44 |
+
manifest = directory / "manifest.json"
|
| 45 |
+
if not manifest.is_file():
|
| 46 |
+
raise ExperimentEvaluationError(f"experiment not found: {experiment_id}")
|
| 47 |
+
return directory
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def read_experiment_evaluation(
|
| 51 |
+
research_root: Path, experiment_id: str
|
| 52 |
+
) -> dict[str, object]:
|
| 53 |
+
"""Return the saved evaluation without exposing arbitrary filesystem paths."""
|
| 54 |
+
|
| 55 |
+
directory = _experiment_dir(research_root, experiment_id)
|
| 56 |
+
path = directory / "posthoc" / "evaluation.json"
|
| 57 |
+
if not path.is_file():
|
| 58 |
+
return {"exists": False, "experiment_id": experiment_id}
|
| 59 |
+
try:
|
| 60 |
+
value: Any = json.loads(path.read_text(encoding="utf-8"))
|
| 61 |
+
except (OSError, json.JSONDecodeError) as exc:
|
| 62 |
+
raise ExperimentEvaluationError("saved evaluation is unreadable") from exc
|
| 63 |
+
if not isinstance(value, dict):
|
| 64 |
+
raise ExperimentEvaluationError("saved evaluation must be an object")
|
| 65 |
+
return {"exists": True, **value}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def write_experiment_evaluation(
|
| 69 |
+
research_root: Path, payload: dict[str, object]
|
| 70 |
+
) -> dict[str, object]:
|
| 71 |
+
"""Write a versioned interpretation artifact linked from the manifest."""
|
| 72 |
+
|
| 73 |
+
experiment_id = _text(payload, "experiment_id", required=True)
|
| 74 |
+
directory = _experiment_dir(research_root, experiment_id)
|
| 75 |
+
answer = _text(payload, "hypothesis_answer", required=True)
|
| 76 |
+
if answer not in _ANSWERS:
|
| 77 |
+
raise ExperimentEvaluationError("invalid hypothesis_answer")
|
| 78 |
+
|
| 79 |
+
evaluation: dict[str, object] = {
|
| 80 |
+
"schema": "MHRN_POSTHOC_EVALUATION_V1",
|
| 81 |
+
"experiment_id": experiment_id,
|
| 82 |
+
"research_question": _text(payload, "research_question"),
|
| 83 |
+
"hypothesis": _text(payload, "hypothesis"),
|
| 84 |
+
"hypothesis_answer": answer,
|
| 85 |
+
"evaluator": _text(payload, "evaluator", required=True),
|
| 86 |
+
"evaluation": _text(payload, "evaluation", required=True),
|
| 87 |
+
"observations": _text(payload, "observations"),
|
| 88 |
+
"limitations": _text(payload, "limitations"),
|
| 89 |
+
"follow_up": _text(payload, "follow_up"),
|
| 90 |
+
"created_at": _text(payload, "created_at")
|
| 91 |
+
or datetime.now(timezone.utc).isoformat(),
|
| 92 |
+
"scientific_evidence": False,
|
| 93 |
+
"epistemic_layer": "post_hoc_interpretation",
|
| 94 |
+
"human_review_status": "not_reviewed",
|
| 95 |
+
"automatic_evidence_promotion": False,
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
artifact_dir = directory / "posthoc"
|
| 99 |
+
artifact_dir.mkdir(exist_ok=True)
|
| 100 |
+
json_path = artifact_dir / "evaluation.json"
|
| 101 |
+
markdown_path = artifact_dir / "evaluation.md"
|
| 102 |
+
json_path.write_text(
|
| 103 |
+
json.dumps(evaluation, indent=2, ensure_ascii=True) + "\n", encoding="utf-8"
|
| 104 |
+
)
|
| 105 |
+
markdown_path.write_text(_markdown(evaluation), encoding="utf-8")
|
| 106 |
+
|
| 107 |
+
manifest_path = directory / "manifest.json"
|
| 108 |
+
manifest_value: Any = json.loads(manifest_path.read_text(encoding="utf-8"))
|
| 109 |
+
if not isinstance(manifest_value, dict):
|
| 110 |
+
raise ExperimentEvaluationError("experiment manifest must be an object")
|
| 111 |
+
manifest = cast(dict[str, object], manifest_value)
|
| 112 |
+
artifacts_value = manifest.get("artifacts")
|
| 113 |
+
artifacts = (
|
| 114 |
+
cast(dict[str, object], artifacts_value)
|
| 115 |
+
if isinstance(artifacts_value, dict)
|
| 116 |
+
else {}
|
| 117 |
+
)
|
| 118 |
+
artifacts["posthoc_evaluation"] = "posthoc/evaluation.md"
|
| 119 |
+
artifacts["posthoc_evaluation_data"] = "posthoc/evaluation.json"
|
| 120 |
+
manifest["artifacts"] = artifacts
|
| 121 |
+
manifest["posthoc_evaluation"] = {
|
| 122 |
+
"status": "recorded_interpretation_not_evidence",
|
| 123 |
+
"hypothesis_answer": answer,
|
| 124 |
+
"updated_at": evaluation["created_at"],
|
| 125 |
+
}
|
| 126 |
+
manifest_path.write_text(
|
| 127 |
+
json.dumps(manifest, indent=2, ensure_ascii=True) + "\n",
|
| 128 |
+
encoding="utf-8",
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
return {
|
| 132 |
+
"ok": True,
|
| 133 |
+
"experiment_id": experiment_id,
|
| 134 |
+
"markdown_path": f"experiments/{experiment_id}/posthoc/evaluation.md",
|
| 135 |
+
"json_path": f"experiments/{experiment_id}/posthoc/evaluation.json",
|
| 136 |
+
"scientific_evidence": False,
|
| 137 |
+
"epistemic_layer": "post_hoc_interpretation",
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _markdown(evaluation: dict[str, object]) -> str:
|
| 142 |
+
def value(key: str, fallback: str = "Nicht angegeben") -> str:
|
| 143 |
+
return str(evaluation.get(key) or fallback)
|
| 144 |
+
|
| 145 |
+
return (
|
| 146 |
+
"# Post-hoc wissenschaftliche Auswertung\n\n"
|
| 147 |
+
f"- Experiment: `{value('experiment_id')}`\n"
|
| 148 |
+
f"- Forschungsfrage: {value('research_question')}\n"
|
| 149 |
+
f"- Hypothese: {value('hypothesis')}\n"
|
| 150 |
+
f"- Hypothesenantwort: **{value('hypothesis_answer')}**\n"
|
| 151 |
+
f"- Auswertende Person: {value('evaluator')}\n"
|
| 152 |
+
f"- Zeitpunkt: {value('created_at')}\n\n"
|
| 153 |
+
"## Wissenschaftliche Auswertung\n\n"
|
| 154 |
+
f"{value('evaluation')}\n\n"
|
| 155 |
+
"## Beobachtungen und Auswertungskriterien\n\n"
|
| 156 |
+
f"{value('observations')}\n\n"
|
| 157 |
+
"## Limitationen und Alternativerklärungen\n\n"
|
| 158 |
+
f"{value('limitations')}\n\n"
|
| 159 |
+
"## Nächste Schritte\n\n"
|
| 160 |
+
f"{value('follow_up')}\n\n"
|
| 161 |
+
"## Evidenzgrenze\n\n"
|
| 162 |
+
"Dieses Dokument ist eine nachgelagerte Interpretation. Es ist keine "
|
| 163 |
+
"automatische EVID-Einstufung, verändert keine Hypothesenregistrierung "
|
| 164 |
+
"und benötigt einen separaten Human-Review-/EVID-Workflow, bevor es als "
|
| 165 |
+
"wissenschaftlich geprüfte Aussage in die Arbeit eingehen kann.\n"
|
| 166 |
+
)
|
src/dashboard/gate_status.py
CHANGED
|
@@ -720,7 +720,7 @@ class GateStatusBuilder:
|
|
| 720 |
gate=GATE_A,
|
| 721 |
id="A-SINGLE-LISTENER",
|
| 722 |
category="technical_integration",
|
| 723 |
-
label="Exactly one TCP LISTEN socket on 127.0.0.1:
|
| 724 |
status=G_PASSED if listener_verified else G_PENDING,
|
| 725 |
maturity=VERIFIED if listener_verified else IMPLEMENTED,
|
| 726 |
source=(
|
|
|
|
| 720 |
gate=GATE_A,
|
| 721 |
id="A-SINGLE-LISTENER",
|
| 722 |
category="technical_integration",
|
| 723 |
+
label="Exactly one TCP LISTEN socket on 127.0.0.1:8767 owned by MHRN PID",
|
| 724 |
status=G_PASSED if listener_verified else G_PENDING,
|
| 725 |
maturity=VERIFIED if listener_verified else IMPLEMENTED,
|
| 726 |
source=(
|
src/dashboard/integration_status.py
CHANGED
|
@@ -16,6 +16,7 @@ A component disabled by config is NEVER reported as "failed".
|
|
| 16 |
|
| 17 |
from __future__ import annotations
|
| 18 |
|
|
|
|
| 19 |
from pathlib import Path
|
| 20 |
from typing import Any, cast
|
| 21 |
|
|
@@ -128,6 +129,206 @@ class IntegrationStatusBuilder:
|
|
| 128 |
"total": len(items),
|
| 129 |
"items": cast(JSONValue, items),
|
| 130 |
"source": "live_backend",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
}
|
| 132 |
|
| 133 |
# ------------------------------------------------------------------------
|
|
|
|
| 16 |
|
| 17 |
from __future__ import annotations
|
| 18 |
|
| 19 |
+
import json
|
| 20 |
from pathlib import Path
|
| 21 |
from typing import Any, cast
|
| 22 |
|
|
|
|
| 129 |
"total": len(items),
|
| 130 |
"items": cast(JSONValue, items),
|
| 131 |
"source": "live_backend",
|
| 132 |
+
"acceleration": cast(JSONValue, self._build_acceleration_status()),
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
def _build_acceleration_status(self) -> dict[str, JSONValue]:
|
| 136 |
+
"""Project canonical CUDA/FE-3 integration state without executing GPU work."""
|
| 137 |
+
|
| 138 |
+
manifest_path = (
|
| 139 |
+
self.repo_root
|
| 140 |
+
/ "research"
|
| 141 |
+
/ "verification"
|
| 142 |
+
/ "frozen_environment"
|
| 143 |
+
/ "FE3_DETERMINISTIC_TARGET_V1.json"
|
| 144 |
+
)
|
| 145 |
+
manifest_status: dict[str, JSONValue] = {
|
| 146 |
+
"path": str(manifest_path.relative_to(self.repo_root)),
|
| 147 |
+
"status": "missing",
|
| 148 |
+
"valid": False,
|
| 149 |
+
}
|
| 150 |
+
try:
|
| 151 |
+
from src.verification.frozen_environment import load_manifest_artifact
|
| 152 |
+
|
| 153 |
+
manifest = load_manifest_artifact(manifest_path)
|
| 154 |
+
manifest_status = {
|
| 155 |
+
"path": str(manifest_path.relative_to(self.repo_root)),
|
| 156 |
+
"status": "verified",
|
| 157 |
+
"valid": True,
|
| 158 |
+
"mode": manifest.mode.value,
|
| 159 |
+
"contract_id": manifest.contract_id,
|
| 160 |
+
"manifest_sha256": manifest.manifest_sha256,
|
| 161 |
+
"environment_id": manifest.environment_id,
|
| 162 |
+
"sensor_ticks": len(manifest.sensor_schedule),
|
| 163 |
+
}
|
| 164 |
+
except (OSError, ValueError) as exc:
|
| 165 |
+
manifest_status["message"] = str(exc)
|
| 166 |
+
|
| 167 |
+
backend_status: dict[str, JSONValue]
|
| 168 |
+
try:
|
| 169 |
+
from src.acceleration.cuda import (
|
| 170 |
+
CUDABackend,
|
| 171 |
+
execution_backend_contract_check,
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
backend = CUDABackend()
|
| 175 |
+
capabilities = backend.capabilities().to_mapping()
|
| 176 |
+
backend_status = {
|
| 177 |
+
"status": "integrated",
|
| 178 |
+
"contract_ok": execution_backend_contract_check(),
|
| 179 |
+
"backend_name": backend.backend_name,
|
| 180 |
+
"backend_version": backend.backend_version,
|
| 181 |
+
"capabilities": cast(JSONValue, capabilities),
|
| 182 |
+
}
|
| 183 |
+
except Exception as exc: # fail closed in dashboard projection
|
| 184 |
+
backend_status = {
|
| 185 |
+
"status": "unavailable",
|
| 186 |
+
"contract_ok": False,
|
| 187 |
+
"message": str(exc),
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
reports = sorted(
|
| 191 |
+
(self.repo_root / "docs" / "canonical").glob("HARDWARE_ACCEPTANCE_*.json")
|
| 192 |
+
)
|
| 193 |
+
hardware: dict[str, JSONValue]
|
| 194 |
+
if not reports:
|
| 195 |
+
hardware = {
|
| 196 |
+
"status": "pending",
|
| 197 |
+
"accepted": False,
|
| 198 |
+
"artifact": None,
|
| 199 |
+
"message": "No reviewed physical hardware acceptance artifact is present.",
|
| 200 |
+
}
|
| 201 |
+
else:
|
| 202 |
+
latest = reports[-1]
|
| 203 |
+
try:
|
| 204 |
+
raw_object: object = json.loads(latest.read_text(encoding="utf-8"))
|
| 205 |
+
if not isinstance(raw_object, dict):
|
| 206 |
+
raise ValueError("hardware acceptance artifact must be an object")
|
| 207 |
+
raw = cast(dict[str, object], raw_object)
|
| 208 |
+
accepted = bool(raw.get("passed")) and bool(
|
| 209 |
+
raw.get("full_fe3_accepted")
|
| 210 |
+
)
|
| 211 |
+
hardware = {
|
| 212 |
+
"status": "passed" if accepted else "failed",
|
| 213 |
+
"accepted": accepted,
|
| 214 |
+
"artifact": str(latest.relative_to(self.repo_root)),
|
| 215 |
+
"gpu_identity": cast(JSONValue, raw.get("gpu_identity")),
|
| 216 |
+
"wave4_physical": cast(JSONValue, raw.get("wave4_physical")),
|
| 217 |
+
"builder_d3c_bridge": cast(
|
| 218 |
+
JSONValue, raw.get("builder_d3c_bridge")
|
| 219 |
+
),
|
| 220 |
+
"frozen_environment_fe3": cast(
|
| 221 |
+
JSONValue, raw.get("frozen_environment_fe3")
|
| 222 |
+
),
|
| 223 |
+
"full_fe3_accepted": bool(raw.get("full_fe3_accepted")),
|
| 224 |
+
"scientific_evidence": False,
|
| 225 |
+
}
|
| 226 |
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
| 227 |
+
hardware = {
|
| 228 |
+
"status": "failed",
|
| 229 |
+
"accepted": False,
|
| 230 |
+
"artifact": str(latest.relative_to(self.repo_root)),
|
| 231 |
+
"message": str(exc),
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
capabilities_object = backend_status.get("capabilities")
|
| 235 |
+
backend_live = False
|
| 236 |
+
if isinstance(capabilities_object, dict):
|
| 237 |
+
capabilities_mapping = capabilities_object
|
| 238 |
+
backend_live = (
|
| 239 |
+
capabilities_mapping.get("supports_live_external_input") is True
|
| 240 |
+
)
|
| 241 |
+
fe3_software_ready = bool(manifest_status["valid"]) and backend_live
|
| 242 |
+
|
| 243 |
+
try:
|
| 244 |
+
from src.homeostasis.pan_wave5b_preflight import (
|
| 245 |
+
evaluate_pan_wave5b_readiness,
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
pan_wave5b = evaluate_pan_wave5b_readiness(self.repo_root).to_mapping()
|
| 249 |
+
except Exception as exc: # fail closed in dashboard projection
|
| 250 |
+
pan_wave5b = {
|
| 251 |
+
"classification": "PAN_WAVE5B_PREFLIGHT",
|
| 252 |
+
"scientific_evidence": False,
|
| 253 |
+
"integration_claimed": False,
|
| 254 |
+
"preflight_ready": False,
|
| 255 |
+
"ready_for_execution": False,
|
| 256 |
+
"blockers": ["PAN_WAVE5B_PREFLIGHT_UNAVAILABLE"],
|
| 257 |
+
"next_gate": "REPAIR_PAN_WAVE5B_PREFLIGHT",
|
| 258 |
+
"message": str(exc),
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
pan_preflight_ready = pan_wave5b.get("preflight_ready") is True
|
| 262 |
+
|
| 263 |
+
waves: list[dict[str, JSONValue]] = [
|
| 264 |
+
{"id": "wave1", "label": "Neural I/O Contracts", "status": "integrated"},
|
| 265 |
+
{
|
| 266 |
+
"id": "wave2",
|
| 267 |
+
"label": "Codecs / Adapter / Gateway",
|
| 268 |
+
"status": "integrated",
|
| 269 |
+
},
|
| 270 |
+
{
|
| 271 |
+
"id": "wave3",
|
| 272 |
+
"label": "ExecutionBackend / Parity / Determinism",
|
| 273 |
+
"status": "integrated",
|
| 274 |
+
},
|
| 275 |
+
{"id": "wave4", "label": "Canonical CUDA Backend", "status": "integrated"},
|
| 276 |
+
{
|
| 277 |
+
"id": "fe3",
|
| 278 |
+
"label": "FE-3 Live Backend Bridge",
|
| 279 |
+
"status": "software_verified" if fe3_software_ready else "blocked",
|
| 280 |
+
},
|
| 281 |
+
{
|
| 282 |
+
"id": "hardware",
|
| 283 |
+
"label": "Physical RTX Hardware Acceptance",
|
| 284 |
+
"status": str(hardware["status"]),
|
| 285 |
+
},
|
| 286 |
+
{
|
| 287 |
+
"id": "wave5",
|
| 288 |
+
"label": "PAN Hyperstate / Wave 5B",
|
| 289 |
+
"status": (
|
| 290 |
+
"preflight_ready" if pan_preflight_ready else "contract_draft"
|
| 291 |
+
),
|
| 292 |
+
},
|
| 293 |
+
{"id": "wave6", "label": "Structural Plasticity", "status": "blocked"},
|
| 294 |
+
{
|
| 295 |
+
"id": "wave7",
|
| 296 |
+
"label": "Canonical Learning Contract",
|
| 297 |
+
"status": "blocked",
|
| 298 |
+
},
|
| 299 |
+
]
|
| 300 |
+
|
| 301 |
+
return {
|
| 302 |
+
"classification": "MHRN_ACCELERATION_INTEGRATION_STATUS",
|
| 303 |
+
"scientific_evidence": False,
|
| 304 |
+
"software_path_closed": fe3_software_ready,
|
| 305 |
+
"physical_hardware_accepted": bool(hardware["accepted"]),
|
| 306 |
+
"provenance_rule": (
|
| 307 |
+
"CPU/CUDA execution fingerprints differ by design; "
|
| 308 |
+
"semantic equality is manifest + live-input + D3c parity."
|
| 309 |
+
),
|
| 310 |
+
"backend": cast(JSONValue, backend_status),
|
| 311 |
+
"fe3_manifest": cast(JSONValue, manifest_status),
|
| 312 |
+
"hardware_acceptance": cast(JSONValue, hardware),
|
| 313 |
+
"pan_wave5b_preflight": cast(JSONValue, pan_wave5b),
|
| 314 |
+
"waves": cast(JSONValue, waves),
|
| 315 |
+
"next_gate": str(
|
| 316 |
+
pan_wave5b.get(
|
| 317 |
+
"next_gate",
|
| 318 |
+
"REPAIR_PAN_WAVE5B_PREFLIGHT",
|
| 319 |
+
)
|
| 320 |
+
),
|
| 321 |
+
"limitations": cast(
|
| 322 |
+
JSONValue,
|
| 323 |
+
{
|
| 324 |
+
"scientific_data": False,
|
| 325 |
+
"evidence": False,
|
| 326 |
+
"speedup_claim": False,
|
| 327 |
+
"pan_hyperstate": False,
|
| 328 |
+
"structural_plasticity": False,
|
| 329 |
+
"canonical_learning_contract": False,
|
| 330 |
+
},
|
| 331 |
+
),
|
| 332 |
}
|
| 333 |
|
| 334 |
# ------------------------------------------------------------------------
|
src/dashboard/network_settings.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Central network settings for the MHRN dashboard.
|
| 2 |
+
|
| 3 |
+
All components (launcher, server, start scripts, Hugging Face config)
|
| 4 |
+
read their default host, port and LAN policy from this single module.
|
| 5 |
+
Change values here to reconfigure the entire stack.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
# ---------------------------------------------------------------------------
|
| 11 |
+
# Dashboard HTTP server
|
| 12 |
+
# ---------------------------------------------------------------------------
|
| 13 |
+
|
| 14 |
+
DASHBOARD_HOST: str = "127.0.0.1"
|
| 15 |
+
"""Default bind address for direct Python dashboard startup."""
|
| 16 |
+
|
| 17 |
+
DASHBOARD_LAN_HOST: str = "0.0.0.0" # nosec B104 - trusted-LAN wrapper default
|
| 18 |
+
"""Trusted-LAN bind address used by the Windows convenience wrappers.
|
| 19 |
+
|
| 20 |
+
The Windows launchers intentionally expose the dashboard to the host's private
|
| 21 |
+
network by default. Direct Python startup remains loopback-only unless the user
|
| 22 |
+
passes an explicit host.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
DASHBOARD_PORT: int = 8767
|
| 26 |
+
"""Default TCP port for the dashboard HTTP server."""
|
| 27 |
+
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
# Public / Internet policy
|
| 30 |
+
# ---------------------------------------------------------------------------
|
| 31 |
+
|
| 32 |
+
ALLOW_PUBLIC_DEPLOYMENT: bool = False
|
| 33 |
+
"""Whether the dashboard is allowed to bind to a public interface.
|
| 34 |
+
|
| 35 |
+
Set this to ``True`` only when a proper reverse proxy, TLS, authentication,
|
| 36 |
+
logging and privacy notice are in place. The dashboard exposes operator,
|
| 37 |
+
research and file-management endpoints without built-in access control.
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
PUBLIC_DEPLOYMENT_NOTE: str = (
|
| 41 |
+
"The dashboard exposes operator, research and file-management capabilities. "
|
| 42 |
+
"Do not expose it directly to the public Internet. For trusted-LAN use, "
|
| 43 |
+
f"restrict TCP port {DASHBOARD_PORT} to the intended private network."
|
| 44 |
+
)
|
| 45 |
+
"""Safety note displayed in the README and referenced in deployment docs."""
|
| 46 |
+
|
| 47 |
+
# ---------------------------------------------------------------------------
|
| 48 |
+
# Hugging Face Space
|
| 49 |
+
# ---------------------------------------------------------------------------
|
| 50 |
+
|
| 51 |
+
HF_SPACE_PORT: int = DASHBOARD_PORT
|
| 52 |
+
"""Port used inside the Hugging Face Docker Space (must match EXPOSE)."""
|
| 53 |
+
|
| 54 |
+
HF_SPACE_HOST: str = "0.0.0.0" # nosec B104 - required container bind for HF Spaces
|
| 55 |
+
"""Host used inside the Hugging Face Space container."""
|
| 56 |
+
|
| 57 |
+
# ---------------------------------------------------------------------------
|
| 58 |
+
# Convenience accessors
|
| 59 |
+
# ---------------------------------------------------------------------------
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def dashboard_local_url() -> str:
|
| 63 |
+
"""Return the loopback URL of the dashboard."""
|
| 64 |
+
return f"http://127.0.0.1:{DASHBOARD_PORT}"
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def dashboard_bind_spec() -> str:
|
| 68 |
+
"""Return the ``host:port`` string used for bind / firewall docs."""
|
| 69 |
+
return f"{DASHBOARD_HOST}:{DASHBOARD_PORT}"
|
src/dashboard/playground_api.py
CHANGED
|
@@ -2,17 +2,42 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
import threading
|
| 6 |
import time
|
| 7 |
from collections import deque
|
| 8 |
-
from collections.abc import Mapping
|
|
|
|
| 9 |
from typing import cast
|
| 10 |
|
| 11 |
from src.playground import service
|
| 12 |
-
from src.playground.cuda import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
from src.playground.models import PlaygroundConfig
|
| 14 |
from src.playground.night_run import NightRunManager
|
| 15 |
from src.playground.pan import PANEmbodiedSandboxSession, PANSessionDaemon
|
|
|
|
|
|
|
| 16 |
|
| 17 |
_MAX_CONCURRENT_RUNS = 2
|
| 18 |
_RUNS_PER_MINUTE = 20
|
|
@@ -98,20 +123,29 @@ def _reserve_rate_slot() -> None:
|
|
| 98 |
_RECENT_RUNS.append(now)
|
| 99 |
|
| 100 |
|
| 101 |
-
def
|
|
|
|
|
|
|
| 102 |
_reserve_rate_slot()
|
| 103 |
if not _RUN_SEMAPHORE.acquire(blocking=False):
|
| 104 |
raise PlaygroundBusyError(
|
| 105 |
"Playground is busy; at most two simulation requests run concurrently."
|
| 106 |
)
|
| 107 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
if path == "/api/playground/run":
|
| 109 |
return service.run(payload)
|
| 110 |
if path == "/api/playground/robustness":
|
| 111 |
return service.robustness(payload)
|
| 112 |
raise ValueError(f"unknown Playground POST route: {path}")
|
| 113 |
-
|
| 114 |
-
|
| 115 |
|
| 116 |
|
| 117 |
def _live_parts(path: str) -> list[str]:
|
|
@@ -121,9 +155,314 @@ def _live_parts(path: str) -> list[str]:
|
|
| 121 |
return [part for part in path[len(prefix) :].split("/") if part]
|
| 122 |
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
def get_playground(path: str) -> dict[str, object] | None:
|
| 125 |
if path == "/api/playground/catalog":
|
| 126 |
return service.catalog()
|
|
|
|
|
|
|
| 127 |
if path == "/api/playground/sessions":
|
| 128 |
return service.sessions()
|
| 129 |
if path.startswith("/api/playground/sessions/"):
|
|
@@ -131,6 +470,8 @@ def get_playground(path: str) -> dict[str, object] | None:
|
|
| 131 |
return service.replay(session_id)
|
| 132 |
if path == "/api/playground/night":
|
| 133 |
return _NIGHT_RUN.status()
|
|
|
|
|
|
|
| 134 |
if path == "/api/playground/live":
|
| 135 |
return {
|
| 136 |
"class": "PLAYGROUND_LIVE_SESSIONS",
|
|
@@ -149,6 +490,15 @@ def post_playground(
|
|
| 149 |
if path in {"/api/playground/run", "/api/playground/robustness"}:
|
| 150 |
return _bounded_run(path, payload)
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
if path == "/api/playground/determinism":
|
| 153 |
return service.determinism(payload)
|
| 154 |
|
|
@@ -164,6 +514,21 @@ def post_playground(
|
|
| 164 |
ptx_version=ptx_version,
|
| 165 |
).to_mapping()
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
if path == "/api/playground/night/start":
|
| 168 |
return _NIGHT_RUN.start(
|
| 169 |
hours=_payload_float(payload, "hours", 8.0),
|
|
@@ -205,7 +570,11 @@ def post_playground(
|
|
| 205 |
session = _LIVE_DAEMON.get(session_id)
|
| 206 |
if action == "step":
|
| 207 |
ticks = _payload_int(payload, "ticks", 32)
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
if action == "input":
|
| 210 |
values = _numeric_list(payload.get("values", []), "values")
|
| 211 |
duration = _payload_int(payload, "duration_ticks", 16)
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import shutil
|
| 6 |
import threading
|
| 7 |
import time
|
| 8 |
from collections import deque
|
| 9 |
+
from collections.abc import Callable, Mapping
|
| 10 |
+
from dataclasses import replace
|
| 11 |
from typing import cast
|
| 12 |
|
| 13 |
from src.playground import service
|
| 14 |
+
from src.playground.cuda import (
|
| 15 |
+
CompileBundle,
|
| 16 |
+
CudaDriver,
|
| 17 |
+
CudaDriverError,
|
| 18 |
+
CudaRuntimeUnavailable,
|
| 19 |
+
GateLaunchInputs,
|
| 20 |
+
compile_mapping,
|
| 21 |
+
cpu_gate_reference,
|
| 22 |
+
execute_gate_bundle,
|
| 23 |
+
gate_execution_parity_summary,
|
| 24 |
+
preflight_bundle,
|
| 25 |
+
validate_cuda_block_size,
|
| 26 |
+
)
|
| 27 |
+
from src.playground.cuda.builder_parity import run_builder_parity
|
| 28 |
+
from src.playground.cuda.recurrent import (
|
| 29 |
+
cpu_recurrent_reference,
|
| 30 |
+
execute_recurrent,
|
| 31 |
+
recurrent_fixture,
|
| 32 |
+
recurrent_parity,
|
| 33 |
+
)
|
| 34 |
+
from src.playground.cuda.synapses import SynapseConfig
|
| 35 |
+
from src.playground.integration import integration_catalog, transfer_element
|
| 36 |
from src.playground.models import PlaygroundConfig
|
| 37 |
from src.playground.night_run import NightRunManager
|
| 38 |
from src.playground.pan import PANEmbodiedSandboxSession, PANSessionDaemon
|
| 39 |
+
from src.playground.pan.cue_controls import run_cue_controls
|
| 40 |
+
from src.playground.pan.transfer import run_synaptic_transfer
|
| 41 |
|
| 42 |
_MAX_CONCURRENT_RUNS = 2
|
| 43 |
_RUNS_PER_MINUTE = 20
|
|
|
|
| 123 |
_RECENT_RUNS.append(now)
|
| 124 |
|
| 125 |
|
| 126 |
+
def _bounded_operation(
|
| 127 |
+
operation: Callable[[], dict[str, object]],
|
| 128 |
+
) -> dict[str, object]:
|
| 129 |
_reserve_rate_slot()
|
| 130 |
if not _RUN_SEMAPHORE.acquire(blocking=False):
|
| 131 |
raise PlaygroundBusyError(
|
| 132 |
"Playground is busy; at most two simulation requests run concurrently."
|
| 133 |
)
|
| 134 |
try:
|
| 135 |
+
return operation()
|
| 136 |
+
finally:
|
| 137 |
+
_RUN_SEMAPHORE.release()
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def _bounded_run(path: str, payload: Mapping[str, object]) -> dict[str, object]:
|
| 141 |
+
def operation() -> dict[str, object]:
|
| 142 |
if path == "/api/playground/run":
|
| 143 |
return service.run(payload)
|
| 144 |
if path == "/api/playground/robustness":
|
| 145 |
return service.robustness(payload)
|
| 146 |
raise ValueError(f"unknown Playground POST route: {path}")
|
| 147 |
+
|
| 148 |
+
return _bounded_operation(operation)
|
| 149 |
|
| 150 |
|
| 151 |
def _live_parts(path: str) -> list[str]:
|
|
|
|
| 155 |
return [part for part in path[len(prefix) :].split("/") if part]
|
| 156 |
|
| 157 |
|
| 158 |
+
def _cuda_runtime_status() -> dict[str, object]:
|
| 159 |
+
ptxas_path = shutil.which("ptxas")
|
| 160 |
+
driver_available = False
|
| 161 |
+
driver_error: str | None = None
|
| 162 |
+
try:
|
| 163 |
+
driver = CudaDriver()
|
| 164 |
+
driver.initialize()
|
| 165 |
+
driver_available = True
|
| 166 |
+
except (CudaRuntimeUnavailable, CudaDriverError, OSError) as exc:
|
| 167 |
+
driver_error = str(exc)
|
| 168 |
+
|
| 169 |
+
return {
|
| 170 |
+
"classification": "PLAYGROUND_CUDA_DIAGNOSTICS_STATUS",
|
| 171 |
+
"scientific_evidence": False,
|
| 172 |
+
"canonical_cuda_backend": False,
|
| 173 |
+
"target_default": "sm_86",
|
| 174 |
+
"ptxas_available": ptxas_path is not None,
|
| 175 |
+
"ptxas_path": ptxas_path,
|
| 176 |
+
"cuda_driver_available": driver_available,
|
| 177 |
+
"cuda_driver_error": driver_error,
|
| 178 |
+
"stages": {
|
| 179 |
+
"CUDA-1.0": "PTXAS_LOAD_PREFLIGHT_IMPLEMENTED",
|
| 180 |
+
"CUDA-1.1": "DETERMINISM_FREEZE_PARITY_CONTRACT_IMPLEMENTED",
|
| 181 |
+
"CUDA-1.2": "LAUNCH_ABI_BUFFER_VALIDATION_IMPLEMENTED",
|
| 182 |
+
"CUDA-1.3": "CPU_GATE_REFERENCE_D2_IMPLEMENTED_HARDWARE_VERIFICATION_REQUIRED",
|
| 183 |
+
"CUDA-1.4": "BOUNDED_RECURRENT_FP64_REFERENCE_IMPLEMENTED",
|
| 184 |
+
"CUDA-1.5": "BOUNDED_FROZEN_REWARD_PLASTICITY_REFERENCE_IMPLEMENTED",
|
| 185 |
+
"CUDA-1.6": "HYBRID_BUILDER_MEMBRANE_AND_OPTIONAL_PAN_STATE_GPU_BODY_CPU_IMPLEMENTED",
|
| 186 |
+
},
|
| 187 |
+
"application_cpu": {
|
| 188 |
+
"sandbox_physics": True,
|
| 189 |
+
"sensorics": True,
|
| 190 |
+
"actuation": True,
|
| 191 |
+
"posture_analysis": True,
|
| 192 |
+
"reward_triggers": True,
|
| 193 |
+
"closed_loop": True,
|
| 194 |
+
},
|
| 195 |
+
"gpu_porting": {
|
| 196 |
+
"gate_single_tick": True,
|
| 197 |
+
"membrane_state_100_ticks": True,
|
| 198 |
+
"adaptation_state": True,
|
| 199 |
+
"pan_health_energy_hyperstate": "OPTIONAL_CUDA_PAN_BUILDER",
|
| 200 |
+
"pan_apoptosis_decision": "OPTIONAL_CUDA_PAN_BUILDER",
|
| 201 |
+
"pan_feedback_projection": "OPTIONAL_CUDA_PAN_BUILDER",
|
| 202 |
+
"refractory_state": False,
|
| 203 |
+
"synapses": "BUILDER_GPU_EMISSION_PLASTICITY_REWARD_RESIDENT_QUEUE_HOST_TRACE",
|
| 204 |
+
"builder_rng_semantics": "HOST_TRAVERSAL_STREAM_PRESERVED",
|
| 205 |
+
"builder_inhibitory_emission": True,
|
| 206 |
+
"delays": True,
|
| 207 |
+
"builder_delay_queue": "DEVICE_RESIDENT_RING_65",
|
| 208 |
+
"builder_neuron_traces": "DEVICE_RESIDENT_TWO_PHASE",
|
| 209 |
+
"builder_pan_population": "CUDA_ORDERED_AXIS_REDUCTION",
|
| 210 |
+
"plasticity": "BUILDER_PAIR_TRIPLET_ELIGIBILITY_MODULATION_AND_BOUNDED_REFERENCE",
|
| 211 |
+
"sandbox_physics": False,
|
| 212 |
+
},
|
| 213 |
+
"verification": {
|
| 214 |
+
"cpu_gate_reference": True,
|
| 215 |
+
"single_tick_d2_endpoint": True,
|
| 216 |
+
"rng_action_path_endpoint": True,
|
| 217 |
+
"raw_rng_value_parity": True,
|
| 218 |
+
"memory_leak_instrumentation": True,
|
| 219 |
+
},
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def _diagnostic_inputs(
|
| 224 |
+
bundle: CompileBundle,
|
| 225 |
+
*,
|
| 226 |
+
n_neurons: int,
|
| 227 |
+
seed: int,
|
| 228 |
+
epsilon: float,
|
| 229 |
+
) -> GateLaunchInputs:
|
| 230 |
+
manifest = bundle.manifest
|
| 231 |
+
raw_abi = manifest.get("kernel_abi")
|
| 232 |
+
if not isinstance(raw_abi, Mapping):
|
| 233 |
+
raise ValueError("compile bundle is missing kernel_abi")
|
| 234 |
+
abi = cast(Mapping[str, object], raw_abi)
|
| 235 |
+
input_channels = _payload_int(abi, "input_channels", 8)
|
| 236 |
+
action_count = _payload_int(abi, "action_space_size", 4)
|
| 237 |
+
pan_dimensions = _payload_int(abi, "pan_dimensions", 5)
|
| 238 |
+
full_mask = (1 << input_channels) - 1 if input_channels < 64 else 0xFFFFFFFFFFFFFFFF
|
| 239 |
+
logits: list[float] = []
|
| 240 |
+
for neuron in range(n_neurons):
|
| 241 |
+
preferred = neuron % action_count
|
| 242 |
+
for action in range(action_count):
|
| 243 |
+
logits.append(1.0 if action == preferred else 0.05 * (action + 1))
|
| 244 |
+
return GateLaunchInputs.from_sequences(
|
| 245 |
+
input_current=[1.0 + 0.125 * (index % 7) for index in range(n_neurons)],
|
| 246 |
+
channel_masks=[full_mask] * n_neurons,
|
| 247 |
+
amplitudes=[1.0 + 0.05 * channel for channel in range(input_channels)],
|
| 248 |
+
reward_ring=[0.5, -0.25, 0.75, 0.0],
|
| 249 |
+
action_map=[
|
| 250 |
+
0.1 * (action + 1) * (channel + 1)
|
| 251 |
+
for action in range(action_count)
|
| 252 |
+
for channel in range(input_channels)
|
| 253 |
+
],
|
| 254 |
+
feedback_matrix=[
|
| 255 |
+
0.01 * (1 + ((neuron + dimension) % 5))
|
| 256 |
+
for neuron in range(n_neurons)
|
| 257 |
+
for dimension in range(pan_dimensions)
|
| 258 |
+
],
|
| 259 |
+
population=[0.1 * (dimension + 1) for dimension in range(pan_dimensions)],
|
| 260 |
+
logits=logits,
|
| 261 |
+
tick=3,
|
| 262 |
+
target_index=min(2, action_count - 1),
|
| 263 |
+
previous_action=min(1, action_count - 1),
|
| 264 |
+
seed=seed,
|
| 265 |
+
epsilon=epsilon,
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def _cuda_config_payload(payload: Mapping[str, object]) -> dict[str, object]:
|
| 270 |
+
excluded = {
|
| 271 |
+
"target_sm",
|
| 272 |
+
"ptx_version",
|
| 273 |
+
"n_neurons",
|
| 274 |
+
"block_size",
|
| 275 |
+
"device_ordinal",
|
| 276 |
+
"reference_commit",
|
| 277 |
+
"rng_samples",
|
| 278 |
+
}
|
| 279 |
+
return {key: value for key, value in payload.items() if key not in excluded}
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def _cuda_preflight(payload: Mapping[str, object]) -> dict[str, object]:
|
| 283 |
+
target_sm = _payload_text(payload, "target_sm", "sm_86")
|
| 284 |
+
ptx_version = _payload_text(payload, "ptx_version", "7.1")
|
| 285 |
+
n_neurons = _payload_int(payload, "n_neurons", 256)
|
| 286 |
+
block_size = validate_cuda_block_size(_payload_int(payload, "block_size", 64))
|
| 287 |
+
device_ordinal = _payload_int(payload, "device_ordinal", 0)
|
| 288 |
+
if not 1 <= n_neurons <= 12_288:
|
| 289 |
+
raise ValueError("n_neurons must be in [1, 12288]")
|
| 290 |
+
bundle = compile_mapping(
|
| 291 |
+
_cuda_config_payload(payload),
|
| 292 |
+
target_sm=target_sm,
|
| 293 |
+
ptx_version=ptx_version,
|
| 294 |
+
)
|
| 295 |
+
return preflight_bundle(
|
| 296 |
+
bundle,
|
| 297 |
+
n_neurons=n_neurons,
|
| 298 |
+
block_size=block_size,
|
| 299 |
+
device_ordinal=device_ordinal,
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def _cuda_smoke(payload: Mapping[str, object]) -> dict[str, object]:
|
| 304 |
+
target_sm = _payload_text(payload, "target_sm", "sm_86")
|
| 305 |
+
ptx_version = _payload_text(payload, "ptx_version", "7.1")
|
| 306 |
+
n_neurons = _payload_int(payload, "n_neurons", 64)
|
| 307 |
+
block_size = validate_cuda_block_size(_payload_int(payload, "block_size", 64))
|
| 308 |
+
device_ordinal = _payload_int(payload, "device_ordinal", 0)
|
| 309 |
+
reference_commit = _payload_text(payload, "reference_commit", "")
|
| 310 |
+
if not 1 <= n_neurons <= 4096:
|
| 311 |
+
raise ValueError("n_neurons must be in [1, 4096] for hardware smoke")
|
| 312 |
+
bundle = compile_mapping(
|
| 313 |
+
_cuda_config_payload(payload),
|
| 314 |
+
target_sm=target_sm,
|
| 315 |
+
ptx_version=ptx_version,
|
| 316 |
+
)
|
| 317 |
+
inputs = _diagnostic_inputs(
|
| 318 |
+
bundle,
|
| 319 |
+
n_neurons=n_neurons,
|
| 320 |
+
seed=12345,
|
| 321 |
+
epsilon=0.0,
|
| 322 |
+
)
|
| 323 |
+
reference = cpu_gate_reference(bundle, inputs)
|
| 324 |
+
first = execute_gate_bundle(
|
| 325 |
+
bundle,
|
| 326 |
+
inputs,
|
| 327 |
+
block_size=block_size,
|
| 328 |
+
device_ordinal=device_ordinal,
|
| 329 |
+
)
|
| 330 |
+
second = execute_gate_bundle(
|
| 331 |
+
bundle,
|
| 332 |
+
inputs,
|
| 333 |
+
block_size=block_size,
|
| 334 |
+
device_ordinal=device_ordinal,
|
| 335 |
+
)
|
| 336 |
+
first_parity = gate_execution_parity_summary(
|
| 337 |
+
reference,
|
| 338 |
+
first,
|
| 339 |
+
tolerance=1.0e-5,
|
| 340 |
+
reference_commit=reference_commit,
|
| 341 |
+
)
|
| 342 |
+
second_parity = gate_execution_parity_summary(
|
| 343 |
+
reference,
|
| 344 |
+
second,
|
| 345 |
+
tolerance=1.0e-5,
|
| 346 |
+
reference_commit=reference_commit,
|
| 347 |
+
)
|
| 348 |
+
deterministic = first.get("outputs") == second.get("outputs")
|
| 349 |
+
return {
|
| 350 |
+
"classification": "PLAYGROUND_CUDA1_3_HARDWARE_SMOKE",
|
| 351 |
+
"scientific_evidence": False,
|
| 352 |
+
"canonical_cuda_backend": False,
|
| 353 |
+
"reference": reference,
|
| 354 |
+
"first": first,
|
| 355 |
+
"second": second,
|
| 356 |
+
"parity": first_parity,
|
| 357 |
+
"second_parity": second_parity,
|
| 358 |
+
"gpu_repeat_exact": deterministic,
|
| 359 |
+
"passed": bool(first_parity["passed"])
|
| 360 |
+
and bool(second_parity["passed"])
|
| 361 |
+
and deterministic,
|
| 362 |
+
"cleanup_contract": {
|
| 363 |
+
"device_allocations_released_in_finally": True,
|
| 364 |
+
"driver_module_unloaded": True,
|
| 365 |
+
"memory_leak_instrumented": True,
|
| 366 |
+
"first_memory": first.get("memory"),
|
| 367 |
+
"second_memory": second.get("memory"),
|
| 368 |
+
},
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def _cuda_rng_parity(payload: Mapping[str, object]) -> dict[str, object]:
|
| 373 |
+
target_sm = _payload_text(payload, "target_sm", "sm_86")
|
| 374 |
+
ptx_version = _payload_text(payload, "ptx_version", "7.1")
|
| 375 |
+
samples = _payload_int(payload, "rng_samples", 1000)
|
| 376 |
+
block_size = validate_cuda_block_size(_payload_int(payload, "block_size", 64))
|
| 377 |
+
device_ordinal = _payload_int(payload, "device_ordinal", 0)
|
| 378 |
+
if not 1 <= samples <= 4096:
|
| 379 |
+
raise ValueError("rng_samples must be in [1, 4096]")
|
| 380 |
+
bundle = compile_mapping(
|
| 381 |
+
_cuda_config_payload(payload),
|
| 382 |
+
target_sm=target_sm,
|
| 383 |
+
ptx_version=ptx_version,
|
| 384 |
+
)
|
| 385 |
+
inputs = _diagnostic_inputs(
|
| 386 |
+
bundle,
|
| 387 |
+
n_neurons=samples,
|
| 388 |
+
seed=12345,
|
| 389 |
+
epsilon=1.0,
|
| 390 |
+
)
|
| 391 |
+
reference = cpu_gate_reference(bundle, inputs)
|
| 392 |
+
candidate = execute_gate_bundle(
|
| 393 |
+
bundle,
|
| 394 |
+
inputs,
|
| 395 |
+
block_size=block_size,
|
| 396 |
+
device_ordinal=device_ordinal,
|
| 397 |
+
)
|
| 398 |
+
reference_outputs = cast(Mapping[str, object], reference["outputs"])
|
| 399 |
+
candidate_outputs = cast(Mapping[str, object], candidate["outputs"])
|
| 400 |
+
reference_actions = cast(list[int], reference_outputs["action"])
|
| 401 |
+
candidate_actions = cast(list[int], candidate_outputs["action"])
|
| 402 |
+
actions_exact = reference_actions == candidate_actions
|
| 403 |
+
return {
|
| 404 |
+
"classification": "PLAYGROUND_CUDA_RNG_ACTION_PATH_PARITY",
|
| 405 |
+
"scientific_evidence": False,
|
| 406 |
+
"samples": samples,
|
| 407 |
+
"seed": 12345,
|
| 408 |
+
"tick": inputs.tick,
|
| 409 |
+
"epsilon": 1.0,
|
| 410 |
+
"actions_exact": actions_exact,
|
| 411 |
+
"passed": actions_exact,
|
| 412 |
+
"first_reference_actions": reference_actions[:20],
|
| 413 |
+
"first_cuda_actions": candidate_actions[:20],
|
| 414 |
+
"scope": "HASH_EPSILON_GREEDY_ACTION_PATH_UINT24_TO_F32_HALF_OPEN",
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
def _cuda_recurrent_parity(payload: Mapping[str, object]) -> dict[str, object]:
|
| 419 |
+
block_size = validate_cuda_block_size(_payload_int(payload, "block_size", 64))
|
| 420 |
+
inputs = recurrent_fixture(
|
| 421 |
+
n_neurons=_payload_int(payload, "n_neurons", 129),
|
| 422 |
+
ticks=_payload_int(payload, "ticks", 100),
|
| 423 |
+
model=_payload_text(payload, "model", "pan_adex_5d"),
|
| 424 |
+
)
|
| 425 |
+
plasticity = payload.get("plasticity", False)
|
| 426 |
+
if not isinstance(plasticity, bool):
|
| 427 |
+
raise ValueError("plasticity must be boolean")
|
| 428 |
+
if plasticity:
|
| 429 |
+
inputs = replace(
|
| 430 |
+
inputs,
|
| 431 |
+
synapses=SynapseConfig(),
|
| 432 |
+
rewards=tuple(
|
| 433 |
+
1.0 if tick % 32 == 0 else 0.0 for tick in range(inputs.ticks)
|
| 434 |
+
),
|
| 435 |
+
)
|
| 436 |
+
reference = cpu_recurrent_reference(inputs)
|
| 437 |
+
candidate = execute_recurrent(
|
| 438 |
+
inputs,
|
| 439 |
+
block_size=block_size,
|
| 440 |
+
target_sm=_payload_text(payload, "target_sm", "sm_86"),
|
| 441 |
+
)
|
| 442 |
+
return {
|
| 443 |
+
"classification": (
|
| 444 |
+
"PLAYGROUND_CUDA15_PLASTICITY_PARITY"
|
| 445 |
+
if plasticity
|
| 446 |
+
else "PLAYGROUND_CUDA14_RECURRENT_PARITY"
|
| 447 |
+
),
|
| 448 |
+
"scientific_evidence": False,
|
| 449 |
+
"model": inputs.model,
|
| 450 |
+
"ticks": inputs.ticks,
|
| 451 |
+
"state_dtype": candidate["state_dtype"],
|
| 452 |
+
"preflight": candidate["preflight"],
|
| 453 |
+
"parity": recurrent_parity(
|
| 454 |
+
reference, cast(dict[str, object], candidate["outputs"])
|
| 455 |
+
),
|
| 456 |
+
"scope": candidate["comparison_scope"],
|
| 457 |
+
"full_pan_backend": False,
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
|
| 461 |
def get_playground(path: str) -> dict[str, object] | None:
|
| 462 |
if path == "/api/playground/catalog":
|
| 463 |
return service.catalog()
|
| 464 |
+
if path == "/api/playground/integration":
|
| 465 |
+
return integration_catalog()
|
| 466 |
if path == "/api/playground/sessions":
|
| 467 |
return service.sessions()
|
| 468 |
if path.startswith("/api/playground/sessions/"):
|
|
|
|
| 470 |
return service.replay(session_id)
|
| 471 |
if path == "/api/playground/night":
|
| 472 |
return _NIGHT_RUN.status()
|
| 473 |
+
if path == "/api/playground/cuda/status":
|
| 474 |
+
return _cuda_runtime_status()
|
| 475 |
if path == "/api/playground/live":
|
| 476 |
return {
|
| 477 |
"class": "PLAYGROUND_LIVE_SESSIONS",
|
|
|
|
| 490 |
if path in {"/api/playground/run", "/api/playground/robustness"}:
|
| 491 |
return _bounded_run(path, payload)
|
| 492 |
|
| 493 |
+
if path == "/api/playground/integration/transfer":
|
| 494 |
+
return transfer_element(payload)
|
| 495 |
+
|
| 496 |
+
if path == "/api/playground/research/synaptic-transfer":
|
| 497 |
+
return _bounded_operation(lambda: run_synaptic_transfer(payload))
|
| 498 |
+
|
| 499 |
+
if path == "/api/playground/research/cue-controls":
|
| 500 |
+
return _bounded_operation(lambda: run_cue_controls(payload))
|
| 501 |
+
|
| 502 |
if path == "/api/playground/determinism":
|
| 503 |
return service.determinism(payload)
|
| 504 |
|
|
|
|
| 514 |
ptx_version=ptx_version,
|
| 515 |
).to_mapping()
|
| 516 |
|
| 517 |
+
if path == "/api/playground/cuda/preflight":
|
| 518 |
+
return _bounded_operation(lambda: _cuda_preflight(payload))
|
| 519 |
+
|
| 520 |
+
if path == "/api/playground/cuda/smoke":
|
| 521 |
+
return _bounded_operation(lambda: _cuda_smoke(payload))
|
| 522 |
+
|
| 523 |
+
if path == "/api/playground/cuda/rng-parity":
|
| 524 |
+
return _bounded_operation(lambda: _cuda_rng_parity(payload))
|
| 525 |
+
|
| 526 |
+
if path == "/api/playground/cuda/builder-parity":
|
| 527 |
+
return _bounded_operation(lambda: run_builder_parity(payload))
|
| 528 |
+
|
| 529 |
+
if path == "/api/playground/cuda/recurrent-parity":
|
| 530 |
+
return _bounded_operation(lambda: _cuda_recurrent_parity(payload))
|
| 531 |
+
|
| 532 |
if path == "/api/playground/night/start":
|
| 533 |
return _NIGHT_RUN.start(
|
| 534 |
hours=_payload_float(payload, "hours", 8.0),
|
|
|
|
| 570 |
session = _LIVE_DAEMON.get(session_id)
|
| 571 |
if action == "step":
|
| 572 |
ticks = _payload_int(payload, "ticks", 32)
|
| 573 |
+
include_topology = payload.get("include_topology", True) is not False
|
| 574 |
+
return {
|
| 575 |
+
"session_id": session_id,
|
| 576 |
+
**session.step(ticks, include_topology=include_topology),
|
| 577 |
+
}
|
| 578 |
if action == "input":
|
| 579 |
values = _numeric_list(payload.get("values", []), "values")
|
| 580 |
duration = _payload_int(payload, "duration_ticks", 16)
|
src/dashboard/server.py
CHANGED
|
@@ -93,6 +93,11 @@ from .development_timeline import build_development_timeline
|
|
| 93 |
from .docs_source import DocumentationSource, create_docs_source
|
| 94 |
from .embedding_jobs import EmbeddingJobError, list_embedding_jobs, run_embedding_job
|
| 95 |
from .experiment_archive import ExperimentArchiveError, ExperimentArchiveService
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
from .experiment_organizer import ExperimentOrganizerService
|
| 97 |
from .experiment_workflow import (
|
| 98 |
ExperimentWorkflowService,
|
|
@@ -206,6 +211,9 @@ class DashboardServer(ThreadingHTTPServer):
|
|
| 206 |
|
| 207 |
allow_reuse_address = True
|
| 208 |
daemon_threads = True
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
def __init__(
|
| 211 |
self,
|
|
@@ -683,6 +691,11 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
|
|
| 683 |
if path == "/api/research/experiments/archive":
|
| 684 |
self._serve_archived_experiments()
|
| 685 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 686 |
if path == "/api/research/experiment-series":
|
| 687 |
self._serve_experiment_series()
|
| 688 |
return
|
|
@@ -727,6 +740,10 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
|
|
| 727 |
# Integration Status (real backend data, Phase 14)
|
| 728 |
# ----------------------------------------------------------------
|
| 729 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
if path == "/api/integration/status":
|
| 731 |
self._serve_integration_status()
|
| 732 |
return
|
|
@@ -1058,6 +1075,9 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
|
|
| 1058 |
if path == "/api/research/experiments/archive":
|
| 1059 |
self._archive_experiment(body)
|
| 1060 |
return
|
|
|
|
|
|
|
|
|
|
| 1061 |
if path == "/api/research/analysis-jobs":
|
| 1062 |
self._run_analysis_job(body)
|
| 1063 |
return
|
|
@@ -2165,6 +2185,33 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
|
|
| 2165 |
|
| 2166 |
self._send_api_not_found(path)
|
| 2167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2168 |
# ========================================================================
|
| 2169 |
# Integration Status (real backend data, Phase 14)
|
| 2170 |
# ========================================================================
|
|
@@ -4361,7 +4408,7 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
|
|
| 4361 |
tenant = os.environ.get("BRAIN5D_MICROSOFT_TENANT", "common").strip()
|
| 4362 |
redirect_uri = os.environ.get(
|
| 4363 |
"BRAIN5D_MICROSOFT_REDIRECT_URI",
|
| 4364 |
-
"http://127.0.0.1:
|
| 4365 |
).strip()
|
| 4366 |
if not client_id:
|
| 4367 |
self._send_json(
|
|
@@ -4570,6 +4617,32 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
|
|
| 4570 |
{"experiments": cast(list[JSONValue], source.list_experiments())}
|
| 4571 |
)
|
| 4572 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4573 |
def _write_artifact_review(self, body: dict[str, Any]) -> None:
|
| 4574 |
source = self._require_research_source()
|
| 4575 |
artifact_path = body.get("artifact_path")
|
|
@@ -6262,7 +6335,7 @@ def main() -> None:
|
|
| 6262 |
parser.add_argument(
|
| 6263 |
"--port",
|
| 6264 |
type=int,
|
| 6265 |
-
default=
|
| 6266 |
)
|
| 6267 |
|
| 6268 |
parser.add_argument(
|
|
|
|
| 93 |
from .docs_source import DocumentationSource, create_docs_source
|
| 94 |
from .embedding_jobs import EmbeddingJobError, list_embedding_jobs, run_embedding_job
|
| 95 |
from .experiment_archive import ExperimentArchiveError, ExperimentArchiveService
|
| 96 |
+
from .experiment_evaluation import (
|
| 97 |
+
ExperimentEvaluationError,
|
| 98 |
+
read_experiment_evaluation,
|
| 99 |
+
write_experiment_evaluation,
|
| 100 |
+
)
|
| 101 |
from .experiment_organizer import ExperimentOrganizerService
|
| 102 |
from .experiment_workflow import (
|
| 103 |
ExperimentWorkflowService,
|
|
|
|
| 211 |
|
| 212 |
allow_reuse_address = True
|
| 213 |
daemon_threads = True
|
| 214 |
+
# A browser opens bursts of parallel ES-module and asset connections.
|
| 215 |
+
# The stdlib's older backlog of five rejects some on Windows.
|
| 216 |
+
request_queue_size = 64
|
| 217 |
|
| 218 |
def __init__(
|
| 219 |
self,
|
|
|
|
| 691 |
if path == "/api/research/experiments/archive":
|
| 692 |
self._serve_archived_experiments()
|
| 693 |
return
|
| 694 |
+
if path.startswith("/api/research/experiments/") and path.endswith(
|
| 695 |
+
"/evaluation"
|
| 696 |
+
):
|
| 697 |
+
self._serve_experiment_evaluation(path)
|
| 698 |
+
return
|
| 699 |
if path == "/api/research/experiment-series":
|
| 700 |
self._serve_experiment_series()
|
| 701 |
return
|
|
|
|
| 740 |
# Integration Status (real backend data, Phase 14)
|
| 741 |
# ----------------------------------------------------------------
|
| 742 |
|
| 743 |
+
if path == "/api/settings/network":
|
| 744 |
+
self._serve_network_settings()
|
| 745 |
+
return
|
| 746 |
+
|
| 747 |
if path == "/api/integration/status":
|
| 748 |
self._serve_integration_status()
|
| 749 |
return
|
|
|
|
| 1075 |
if path == "/api/research/experiments/archive":
|
| 1076 |
self._archive_experiment(body)
|
| 1077 |
return
|
| 1078 |
+
if path == "/api/research/experiments/evaluation":
|
| 1079 |
+
self._write_experiment_evaluation(body)
|
| 1080 |
+
return
|
| 1081 |
if path == "/api/research/analysis-jobs":
|
| 1082 |
self._run_analysis_job(body)
|
| 1083 |
return
|
|
|
|
| 2185 |
|
| 2186 |
self._send_api_not_found(path)
|
| 2187 |
|
| 2188 |
+
# ========================================================================
|
| 2189 |
+
# Network Settings (central config)
|
| 2190 |
+
# ========================================================================
|
| 2191 |
+
|
| 2192 |
+
def _serve_network_settings(self) -> None:
|
| 2193 |
+
from .network_settings import (
|
| 2194 |
+
ALLOW_PUBLIC_DEPLOYMENT,
|
| 2195 |
+
DASHBOARD_HOST,
|
| 2196 |
+
DASHBOARD_PORT,
|
| 2197 |
+
HF_SPACE_HOST,
|
| 2198 |
+
HF_SPACE_PORT,
|
| 2199 |
+
PUBLIC_DEPLOYMENT_NOTE,
|
| 2200 |
+
)
|
| 2201 |
+
|
| 2202 |
+
self._send_json(
|
| 2203 |
+
{
|
| 2204 |
+
"dashboard_host": DASHBOARD_HOST,
|
| 2205 |
+
"dashboard_port": DASHBOARD_PORT,
|
| 2206 |
+
"local_url": f"http://127.0.0.1:{DASHBOARD_PORT}",
|
| 2207 |
+
"hf_space_host": HF_SPACE_HOST,
|
| 2208 |
+
"hf_space_port": HF_SPACE_PORT,
|
| 2209 |
+
"allow_public_deployment": ALLOW_PUBLIC_DEPLOYMENT,
|
| 2210 |
+
"public_deployment_note": PUBLIC_DEPLOYMENT_NOTE,
|
| 2211 |
+
"source": "src/dashboard/network_settings.py",
|
| 2212 |
+
}
|
| 2213 |
+
)
|
| 2214 |
+
|
| 2215 |
# ========================================================================
|
| 2216 |
# Integration Status (real backend data, Phase 14)
|
| 2217 |
# ========================================================================
|
|
|
|
| 4408 |
tenant = os.environ.get("BRAIN5D_MICROSOFT_TENANT", "common").strip()
|
| 4409 |
redirect_uri = os.environ.get(
|
| 4410 |
"BRAIN5D_MICROSOFT_REDIRECT_URI",
|
| 4411 |
+
"http://127.0.0.1:8767/api/research/chat/oauth/callback",
|
| 4412 |
).strip()
|
| 4413 |
if not client_id:
|
| 4414 |
self._send_json(
|
|
|
|
| 4617 |
{"experiments": cast(list[JSONValue], source.list_experiments())}
|
| 4618 |
)
|
| 4619 |
|
| 4620 |
+
def _serve_experiment_evaluation(self, path: str) -> None:
|
| 4621 |
+
prefix = "/api/research/experiments/"
|
| 4622 |
+
experiment_id = unquote(path[len(prefix) : -len("/evaluation")]).strip("/")
|
| 4623 |
+
try:
|
| 4624 |
+
source = self._require_research_source()
|
| 4625 |
+
self._send_json(
|
| 4626 |
+
cast(
|
| 4627 |
+
Mapping[str, JSONValue],
|
| 4628 |
+
read_experiment_evaluation(source.root(), experiment_id),
|
| 4629 |
+
)
|
| 4630 |
+
)
|
| 4631 |
+
except ExperimentEvaluationError as exc:
|
| 4632 |
+
self._send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
| 4633 |
+
|
| 4634 |
+
def _write_experiment_evaluation(self, body: dict[str, Any]) -> None:
|
| 4635 |
+
source = self._require_research_source()
|
| 4636 |
+
try:
|
| 4637 |
+
result = write_experiment_evaluation(source.root(), body)
|
| 4638 |
+
except ExperimentEvaluationError as exc:
|
| 4639 |
+
self._send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
| 4640 |
+
return
|
| 4641 |
+
self._send_json(
|
| 4642 |
+
cast(Mapping[str, JSONValue], result),
|
| 4643 |
+
HTTPStatus.CREATED,
|
| 4644 |
+
)
|
| 4645 |
+
|
| 4646 |
def _write_artifact_review(self, body: dict[str, Any]) -> None:
|
| 4647 |
source = self._require_research_source()
|
| 4648 |
artifact_path = body.get("artifact_path")
|
|
|
|
| 6335 |
parser.add_argument(
|
| 6336 |
"--port",
|
| 6337 |
type=int,
|
| 6338 |
+
default=8767,
|
| 6339 |
)
|
| 6340 |
|
| 6341 |
parser.add_argument(
|
src/dashboard/static/experiment-workflow-base.js
CHANGED
|
@@ -613,7 +613,9 @@ export class ExperimentWorkflowPanel {
|
|
| 613 |
if (artifacts.calibration) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="calibration" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.calibration)}" title="Kalibrierungsdaten">⚙ Kalibrierung</button>`);
|
| 614 |
if (artifacts.manifest) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="manifest" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.manifest)}" title="Manifest und Provenienz">🧾 Manifest</button>`);
|
| 615 |
if (artifacts.review) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="review" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.review)}" title="Review und wissenschaftliche Weiterverarbeitung">✓ Review</button>`);
|
| 616 |
-
|
|
|
|
|
|
|
| 617 |
const action = archived
|
| 618 |
? `<button type="button" class="btn-small" data-experiment-action="restore" data-experiment-id="${expId}">↶ Wiederherstellen</button>`
|
| 619 |
: `<button type="button" class="btn-small" data-experiment-action="archive" data-experiment-id="${expId}">▣ Archivieren</button>`;
|
|
@@ -685,6 +687,124 @@ export class ExperimentWorkflowPanel {
|
|
| 685 |
return `<details class="experiment-series-item ${item.archived ? "is-archived" : ""}"><summary><span><strong>${escapeHtml(item.series_id)}</strong><small>${escapeHtml(item.created_at || "")}</small></span><em class="series-status-${escapeHtml(item.status)}">${escapeHtml(item.status)} · ${escapeHtml(item.assessment_status)}${archiveLabel}</em></summary><div class="experiment-series-assessment"><div class="experiment-series-kpis"><span><small>Erfolgreich</small><strong>${Number(item.completed || 0)}</strong></span><span><small>Fehlgeschlagen</small><strong>${Number(item.failed || 0)}</strong></span><span><small>Ticks</small><strong>${escapeHtml(item.requested_ticks ?? "—")}</strong></span><span><small>Seeds</small><strong>${escapeHtml(item.seeds ?? "—")}</strong></span></div><p>${escapeHtml(item.assessment_boundary || "Technische Bewertung; Human Review erforderlich.")}</p><ul class="experiment-series-results">${resultRows}</ul><div class="experiment-series-actions">${reportButton}${archiveAction}</div></div></details>`;
|
| 686 |
}
|
| 687 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 688 |
async _handleExperimentLibraryAction(event) {
|
| 689 |
const button = event.target.closest?.("[data-experiment-action], [data-series-action], [data-series-report], [data-experiment-open], [data-experiment-toggle], [data-experiment-use]");
|
| 690 |
if (!button) return;
|
|
@@ -713,6 +833,12 @@ export class ExperimentWorkflowPanel {
|
|
| 713 |
const seriesId = button.dataset.seriesId;
|
| 714 |
const seriesReport = button.dataset.seriesReport;
|
| 715 |
const openKind = button.dataset.experimentOpen;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 716 |
if (seriesReport) {
|
| 717 |
await this._openArtifact(seriesReport);
|
| 718 |
return;
|
|
@@ -1006,6 +1132,7 @@ export class ExperimentWorkflowPanel {
|
|
| 1006 |
if (artifacts.summary) buttons.push({ kind: "summary", label: "Summary", path: artifacts.summary });
|
| 1007 |
if (artifacts.statistics) buttons.push({ kind: "statistics", label: "Statistics", path: artifacts.statistics });
|
| 1008 |
if (artifacts.data_index || artifacts.raw_run_index) buttons.push({ kind: "raw", label: "Raw Index", path: artifacts.data_index || artifacts.raw_run_index });
|
|
|
|
| 1009 |
if (!buttons.length) return;
|
| 1010 |
const group = document.createElement("span");
|
| 1011 |
group.dataset.experimentPopupActions = "true";
|
|
|
|
| 613 |
if (artifacts.calibration) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="calibration" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.calibration)}" title="Kalibrierungsdaten">⚙ Kalibrierung</button>`);
|
| 614 |
if (artifacts.manifest) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="manifest" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.manifest)}" title="Manifest und Provenienz">🧾 Manifest</button>`);
|
| 615 |
if (artifacts.review) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="review" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.review)}" title="Review und wissenschaftliche Weiterverarbeitung">✓ Review</button>`);
|
| 616 |
+
if (artifacts.posthoc_evaluation) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="posthoc" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.posthoc_evaluation)}" title="Gespeicherte wissenschaftliche Auswertung">📝 Auswertung</button>`);
|
| 617 |
+
const evaluationAction = `<button type="button" class="btn-small exp-evaluate-btn" data-experiment-evaluate="${expId}" title="Nachträgliche wissenschaftliche Auswertung erfassen">✎ Auswertung erfassen</button>`;
|
| 618 |
+
const resultActions = `<div class="experiment-library-results">${resultButtons.join("")}${evaluationAction}</div>`;
|
| 619 |
const action = archived
|
| 620 |
? `<button type="button" class="btn-small" data-experiment-action="restore" data-experiment-id="${expId}">↶ Wiederherstellen</button>`
|
| 621 |
: `<button type="button" class="btn-small" data-experiment-action="archive" data-experiment-id="${expId}">▣ Archivieren</button>`;
|
|
|
|
| 687 |
return `<details class="experiment-series-item ${item.archived ? "is-archived" : ""}"><summary><span><strong>${escapeHtml(item.series_id)}</strong><small>${escapeHtml(item.created_at || "")}</small></span><em class="series-status-${escapeHtml(item.status)}">${escapeHtml(item.status)} · ${escapeHtml(item.assessment_status)}${archiveLabel}</em></summary><div class="experiment-series-assessment"><div class="experiment-series-kpis"><span><small>Erfolgreich</small><strong>${Number(item.completed || 0)}</strong></span><span><small>Fehlgeschlagen</small><strong>${Number(item.failed || 0)}</strong></span><span><small>Ticks</small><strong>${escapeHtml(item.requested_ticks ?? "—")}</strong></span><span><small>Seeds</small><strong>${escapeHtml(item.seeds ?? "—")}</strong></span></div><p>${escapeHtml(item.assessment_boundary || "Technische Bewertung; Human Review erforderlich.")}</p><ul class="experiment-series-results">${resultRows}</ul><div class="experiment-series-actions">${reportButton}${archiveAction}</div></div></details>`;
|
| 688 |
}
|
| 689 |
|
| 690 |
+
_ensurePosthocEvaluationDialog() {
|
| 691 |
+
let dialog = byId("workflow-posthoc-evaluation-dialog");
|
| 692 |
+
if (dialog) return dialog;
|
| 693 |
+
dialog = document.createElement("dialog");
|
| 694 |
+
dialog.id = "workflow-posthoc-evaluation-dialog";
|
| 695 |
+
dialog.className = "posthoc-evaluation-dialog";
|
| 696 |
+
dialog.innerHTML = `
|
| 697 |
+
<header class="posthoc-evaluation-header"><div><span class="workspace-kicker">POST-HOC INTERPRETATION</span><h3>Wissenschaftliche Auswertung</h3><p id="posthoc-evaluation-context"></p></div><button type="button" id="posthoc-evaluation-close" class="icon-btn" aria-label="Auswertung schließen">×</button></header>
|
| 698 |
+
<div class="posthoc-evaluation-boundary"><strong>Nachgelagerte Auswertung</strong><span>Diese Antwort verändert weder den Lauf noch den EVID-Status. Sie wird als Interpretation gespeichert und benötigt für wissenschaftliche Verwendung einen separaten Human-Review-/EVID-Workflow.</span></div>
|
| 699 |
+
<div class="posthoc-evaluation-form">
|
| 700 |
+
<label>Auswertende Person<input id="posthoc-evaluation-evaluator" type="text" required placeholder="Name / Verantwortliche Person"></label>
|
| 701 |
+
<label>Antwort auf die Hypothese<select id="posthoc-evaluation-answer"><option value="supported">unterstützt</option><option value="refuted">widerlegt</option><option value="mixed">gemischt / teilweise</option><option value="inconclusive">nicht entscheidbar</option><option value="not_assessed">noch nicht bewertet</option></select></label>
|
| 702 |
+
<label class="posthoc-evaluation-wide">Wissenschaftliche Auswertung<textarea id="posthoc-evaluation-text" rows="8" required placeholder="Welche Auswertung beantwortet die Hypothese? Beziehe dich auf registrierte Outcomes, Kontrollen, Seeds und die beobachteten Daten."></textarea></label>
|
| 703 |
+
<label class="posthoc-evaluation-wide">Beobachtungen und Auswertungskriterien<textarea id="posthoc-evaluation-observations" rows="5" placeholder="Welche Messwerte, Vergleiche oder Artefakte tragen die Antwort?"></textarea></label>
|
| 704 |
+
<label class="posthoc-evaluation-wide">Limitationen und Alternativerklärungen<textarea id="posthoc-evaluation-limitations" rows="5" placeholder="Welche Unsicherheiten, fehlenden Kontrollen oder alternativen Erklärungen bleiben offen?"></textarea></label>
|
| 705 |
+
<label class="posthoc-evaluation-wide">Nächste Schritte für die wissenschaftliche Arbeit<textarea id="posthoc-evaluation-follow-up" rows="5" placeholder="Welche Replikation, Kontrolle, Abbildung oder Textpassage soll daraus entstehen?"></textarea></label>
|
| 706 |
+
</div>
|
| 707 |
+
<footer class="posthoc-evaluation-actions"><span id="posthoc-evaluation-status" role="status"></span><button type="button" id="posthoc-evaluation-open" class="btn-secondary" disabled>Im File Viewer öffnen</button><button type="button" id="posthoc-evaluation-save" class="btn-primary">Auswertung speichern</button></footer>`;
|
| 708 |
+
document.body.append(dialog);
|
| 709 |
+
if (!document.getElementById("posthoc-evaluation-style")) {
|
| 710 |
+
const style = document.createElement("style");
|
| 711 |
+
style.id = "posthoc-evaluation-style";
|
| 712 |
+
style.textContent = `.posthoc-evaluation-dialog{width:min(94vw,880px);max-height:92vh;padding:0;border:1px solid var(--rule-3);border-radius:var(--r-md);background:var(--paper);color:var(--ink);box-shadow:var(--shadow-float)}.posthoc-evaluation-dialog::backdrop{background:rgba(20,16,12,.58);backdrop-filter:blur(4px)}.posthoc-evaluation-header{display:flex;justify-content:space-between;gap:12px;padding:14px 16px;border-bottom:1px solid var(--rule);background:var(--paper-2)}.posthoc-evaluation-header h3{margin:3px 0;font-size:1.05rem}.posthoc-evaluation-header p{margin:0;color:var(--ink-3);font-size:.7rem}.posthoc-evaluation-boundary{margin:12px 16px;padding:9px 11px;border-left:3px solid var(--amber);background:var(--amber-wash);font-size:.7rem;line-height:1.45}.posthoc-evaluation-boundary strong{display:block}.posthoc-evaluation-form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;padding:0 16px 12px;overflow:auto}.posthoc-evaluation-form label{display:grid;gap:5px;color:var(--ink-3);font-size:.68rem}.posthoc-evaluation-form input,.posthoc-evaluation-form select,.posthoc-evaluation-form textarea{width:100%;padding:7px;border:1px solid var(--rule-2);border-radius:var(--r-xs);background:var(--paper-2);color:var(--ink);font:inherit}.posthoc-evaluation-form textarea{resize:vertical;line-height:1.45}.posthoc-evaluation-wide{grid-column:1/-1}.posthoc-evaluation-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:10px 16px;border-top:1px solid var(--rule);background:var(--paper-2)}.posthoc-evaluation-actions span{margin-right:auto;color:var(--ink-3);font-size:.68rem}.posthoc-evaluation-actions button:disabled{opacity:.55}@media(max-width:700px){.posthoc-evaluation-form{grid-template-columns:1fr}.posthoc-evaluation-wide{grid-column:auto}.posthoc-evaluation-actions{flex-wrap:wrap}.posthoc-evaluation-actions span{flex-basis:100%}}`;
|
| 713 |
+
document.head.appendChild(style);
|
| 714 |
+
}
|
| 715 |
+
dialog.querySelector("#posthoc-evaluation-close")?.addEventListener("click", () => dialog.close());
|
| 716 |
+
dialog.querySelector("#posthoc-evaluation-save")?.addEventListener("click", () => this._savePosthocEvaluation());
|
| 717 |
+
dialog.querySelector("#posthoc-evaluation-open")?.addEventListener("click", () => {
|
| 718 |
+
const path = dialog.dataset.markdownPath;
|
| 719 |
+
if (path) this._openArtifact(path);
|
| 720 |
+
});
|
| 721 |
+
return dialog;
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
async _openPosthocEvaluation(item) {
|
| 725 |
+
const dialog = this._ensurePosthocEvaluationDialog();
|
| 726 |
+
const manifest = item.manifest && typeof item.manifest === "object" ? item.manifest : {};
|
| 727 |
+
const experimentId = String(item.experiment_id || item.id || "");
|
| 728 |
+
const questions = Array.isArray(manifest.research_questions) ? manifest.research_questions : [];
|
| 729 |
+
const hypotheses = Array.isArray(manifest.hypotheses) ? manifest.hypotheses : [];
|
| 730 |
+
const researchQuestion = questions.join(", ") || manifest.research_question || "";
|
| 731 |
+
const hypothesis = hypotheses.join(", ") || manifest.hypothesis || "";
|
| 732 |
+
dialog.dataset.experimentId = experimentId;
|
| 733 |
+
dialog.dataset.researchQuestion = String(researchQuestion);
|
| 734 |
+
dialog.dataset.hypothesis = String(hypothesis);
|
| 735 |
+
dialog.dataset.markdownPath = "";
|
| 736 |
+
const set = (id, value) => { const node = byId(id); if (node) node.value = String(value ?? ""); };
|
| 737 |
+
const context = byId("posthoc-evaluation-context");
|
| 738 |
+
if (context) context.textContent = `${experimentId} · ${researchQuestion || "Forschungsfrage nicht hinterlegt"} · ${hypothesis || "Hypothese nicht hinterlegt"}`;
|
| 739 |
+
set("posthoc-evaluation-evaluator", "");
|
| 740 |
+
set("posthoc-evaluation-answer", "not_assessed");
|
| 741 |
+
set("posthoc-evaluation-text", "");
|
| 742 |
+
set("posthoc-evaluation-observations", "");
|
| 743 |
+
set("posthoc-evaluation-limitations", "");
|
| 744 |
+
set("posthoc-evaluation-follow-up", "");
|
| 745 |
+
set("posthoc-evaluation-status", "Lade vorhandene Auswertung …");
|
| 746 |
+
const openButton = byId("posthoc-evaluation-open");
|
| 747 |
+
if (openButton) openButton.disabled = true;
|
| 748 |
+
if (!dialog.open) dialog.showModal();
|
| 749 |
+
try {
|
| 750 |
+
const saved = await fetchJson(`/api/research/experiments/${encodeURIComponent(experimentId)}/evaluation`);
|
| 751 |
+
if (saved.exists) {
|
| 752 |
+
set("posthoc-evaluation-evaluator", saved.evaluator);
|
| 753 |
+
set("posthoc-evaluation-answer", saved.hypothesis_answer || "not_assessed");
|
| 754 |
+
set("posthoc-evaluation-text", saved.evaluation);
|
| 755 |
+
set("posthoc-evaluation-observations", saved.observations);
|
| 756 |
+
set("posthoc-evaluation-limitations", saved.limitations);
|
| 757 |
+
set("posthoc-evaluation-follow-up", saved.follow_up);
|
| 758 |
+
set("posthoc-evaluation-status", "Vorhandene Auswertung geladen.");
|
| 759 |
+
dialog.dataset.markdownPath = `experiments/${experimentId}/posthoc/evaluation.md`;
|
| 760 |
+
if (openButton) openButton.disabled = false;
|
| 761 |
+
} else {
|
| 762 |
+
set("posthoc-evaluation-status", "Noch keine Auswertung erfasst.");
|
| 763 |
+
}
|
| 764 |
+
} catch (error) {
|
| 765 |
+
set("posthoc-evaluation-status", `Auswertung konnte nicht geladen werden: ${error.message || error}`);
|
| 766 |
+
}
|
| 767 |
+
byId("posthoc-evaluation-evaluator")?.focus();
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
async _savePosthocEvaluation() {
|
| 771 |
+
const dialog = byId("workflow-posthoc-evaluation-dialog");
|
| 772 |
+
const experimentId = dialog?.dataset.experimentId;
|
| 773 |
+
if (!dialog || !experimentId) return;
|
| 774 |
+
const value = (id) => byId(id)?.value?.trim() || "";
|
| 775 |
+
const status = byId("posthoc-evaluation-status");
|
| 776 |
+
const saveButton = byId("posthoc-evaluation-save");
|
| 777 |
+
const payload = {
|
| 778 |
+
experiment_id: experimentId,
|
| 779 |
+
research_question: dialog.dataset.researchQuestion || "",
|
| 780 |
+
hypothesis: dialog.dataset.hypothesis || "",
|
| 781 |
+
evaluator: value("posthoc-evaluation-evaluator"),
|
| 782 |
+
hypothesis_answer: value("posthoc-evaluation-answer"),
|
| 783 |
+
evaluation: value("posthoc-evaluation-text"),
|
| 784 |
+
observations: value("posthoc-evaluation-observations"),
|
| 785 |
+
limitations: value("posthoc-evaluation-limitations"),
|
| 786 |
+
follow_up: value("posthoc-evaluation-follow-up"),
|
| 787 |
+
};
|
| 788 |
+
if (!payload.evaluator || !payload.evaluation) {
|
| 789 |
+
if (status) status.textContent = "Auswertende Person und wissenschaftliche Auswertung sind Pflichtfelder.";
|
| 790 |
+
return;
|
| 791 |
+
}
|
| 792 |
+
if (saveButton) { saveButton.disabled = true; saveButton.textContent = "Speichere …"; }
|
| 793 |
+
try {
|
| 794 |
+
const result = await fetchJson("/api/research/experiments/evaluation", { method: "POST", body: JSON.stringify(payload) });
|
| 795 |
+
dialog.dataset.markdownPath = result.markdown_path;
|
| 796 |
+
if (status) status.textContent = "Gespeichert; EVID bleibt unverändert.";
|
| 797 |
+
if (byId("posthoc-evaluation-open")) byId("posthoc-evaluation-open").disabled = false;
|
| 798 |
+
await this._loadExperimentCollections();
|
| 799 |
+
await this._openArtifact(result.markdown_path);
|
| 800 |
+
dialog.close();
|
| 801 |
+
} catch (error) {
|
| 802 |
+
if (status) status.textContent = `Speichern fehlgeschlagen: ${error.message || error}`;
|
| 803 |
+
} finally {
|
| 804 |
+
if (saveButton) { saveButton.disabled = false; saveButton.textContent = "Auswertung speichern"; }
|
| 805 |
+
}
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
async _handleExperimentLibraryAction(event) {
|
| 809 |
const button = event.target.closest?.("[data-experiment-action], [data-series-action], [data-series-report], [data-experiment-open], [data-experiment-toggle], [data-experiment-use]");
|
| 810 |
if (!button) return;
|
|
|
|
| 833 |
const seriesId = button.dataset.seriesId;
|
| 834 |
const seriesReport = button.dataset.seriesReport;
|
| 835 |
const openKind = button.dataset.experimentOpen;
|
| 836 |
+
const evaluateId = button.dataset.experimentEvaluate;
|
| 837 |
+
if (evaluateId) {
|
| 838 |
+
const item = this._findExperimentDetail(evaluateId);
|
| 839 |
+
if (item) await this._openPosthocEvaluation(item);
|
| 840 |
+
return;
|
| 841 |
+
}
|
| 842 |
if (seriesReport) {
|
| 843 |
await this._openArtifact(seriesReport);
|
| 844 |
return;
|
|
|
|
| 1132 |
if (artifacts.summary) buttons.push({ kind: "summary", label: "Summary", path: artifacts.summary });
|
| 1133 |
if (artifacts.statistics) buttons.push({ kind: "statistics", label: "Statistics", path: artifacts.statistics });
|
| 1134 |
if (artifacts.data_index || artifacts.raw_run_index) buttons.push({ kind: "raw", label: "Raw Index", path: artifacts.data_index || artifacts.raw_run_index });
|
| 1135 |
+
if (artifacts.posthoc_evaluation) buttons.push({ kind: "posthoc", label: "Auswertung", path: artifacts.posthoc_evaluation });
|
| 1136 |
if (!buttons.length) return;
|
| 1137 |
const group = document.createElement("span");
|
| 1138 |
group.dataset.experimentPopupActions = "true";
|
src/dashboard/static/frontend/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import { initSmallSNNStage } from "./modules/small-snn-stage.js";
|
|
| 35 |
import { initRecurrentSNNStage } from "./modules/recurrent-snn-stage.js";
|
| 36 |
import { initWorkspaceRouter } from "./workspace-router.js";
|
| 37 |
import { initPlayground } from "./modules/playground.js";
|
|
|
|
| 38 |
import { initExternalReview } from "../external-review.js";
|
| 39 |
|
| 40 |
installPollingGovernor();
|
|
@@ -77,6 +78,7 @@ function init() {
|
|
| 77 |
initRuntimeNeuron();
|
| 78 |
initSmallSNNStage();
|
| 79 |
initRecurrentSNNStage();
|
|
|
|
| 80 |
}
|
| 81 |
|
| 82 |
if (document.readyState === "loading") {
|
|
|
|
| 35 |
import { initRecurrentSNNStage } from "./modules/recurrent-snn-stage.js";
|
| 36 |
import { initWorkspaceRouter } from "./workspace-router.js";
|
| 37 |
import { initPlayground } from "./modules/playground.js";
|
| 38 |
+
import { initAccelerationIntegrationStatus } from "./modules/acceleration-integration.js";
|
| 39 |
import { initExternalReview } from "../external-review.js";
|
| 40 |
|
| 41 |
installPollingGovernor();
|
|
|
|
| 78 |
initRuntimeNeuron();
|
| 79 |
initSmallSNNStage();
|
| 80 |
initRecurrentSNNStage();
|
| 81 |
+
initAccelerationIntegrationStatus();
|
| 82 |
}
|
| 83 |
|
| 84 |
if (document.readyState === "loading") {
|
src/dashboard/static/frontend/modules/acceleration-integration.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
|
| 3 |
+
const STATUS_LABELS = {
|
| 4 |
+
integrated: "integriert",
|
| 5 |
+
software_verified: "Software verifiziert",
|
| 6 |
+
pending: "Hardware ausstehend",
|
| 7 |
+
passed: "physisch akzeptiert",
|
| 8 |
+
failed: "fehlgeschlagen",
|
| 9 |
+
blocked: "blockiert",
|
| 10 |
+
contract_draft: "Semantikvertrag in Arbeit",
|
| 11 |
+
preflight_ready: "Preflight bereit",
|
| 12 |
+
unavailable: "nicht verfügbar",
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
function escapeHtml(value) {
|
| 16 |
+
return String(value ?? "").replace(/[&<>"']/g, (character) => ({
|
| 17 |
+
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
| 18 |
+
}[character]));
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
async function readJson(url) {
|
| 22 |
+
const response = await fetch(url, { cache: "no-store" });
|
| 23 |
+
if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
|
| 24 |
+
return response.json();
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
function statusClass(status) {
|
| 28 |
+
if (status === "integrated" || status === "software_verified" || status === "passed") return "ok";
|
| 29 |
+
if (status === "failed" || status === "unavailable") return "bad";
|
| 30 |
+
return "pending";
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function injectStyles() {
|
| 34 |
+
if (document.getElementById("mhrn-acceleration-integration-styles")) return;
|
| 35 |
+
const style = document.createElement("style");
|
| 36 |
+
style.id = "mhrn-acceleration-integration-styles";
|
| 37 |
+
style.textContent = `
|
| 38 |
+
.mhrn-accel-panel{margin:14px 0;padding:14px;border:1px solid var(--rule);border-radius:var(--r-md);background:var(--paper-2)}
|
| 39 |
+
.mhrn-accel-head{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;margin-bottom:12px}
|
| 40 |
+
.mhrn-accel-head h3{margin:.2rem 0}.mhrn-accel-head p{margin:.2rem 0;max-width:88ch}
|
| 41 |
+
.mhrn-accel-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:9px}
|
| 42 |
+
.mhrn-accel-card{padding:10px;border:1px solid var(--rule);border-radius:8px;background:var(--paper)}
|
| 43 |
+
.mhrn-accel-card h4{margin:0 0 6px}.mhrn-accel-card p{margin:4px 0}
|
| 44 |
+
.mhrn-accel-state{display:inline-flex;padding:2px 7px;border-radius:999px;border:1px solid var(--rule);font-size:.78rem}
|
| 45 |
+
.mhrn-accel-state.ok{font-weight:700}.mhrn-accel-state.bad{font-weight:700}
|
| 46 |
+
.mhrn-accel-meta{display:grid;grid-template-columns:max-content 1fr;gap:4px 10px;margin-top:10px;font-size:.85rem}
|
| 47 |
+
.mhrn-accel-meta dt{font-weight:700}.mhrn-accel-meta dd{margin:0;overflow-wrap:anywhere}
|
| 48 |
+
.mhrn-accel-actions{display:flex;flex-wrap:wrap;gap:7px;margin-top:10px}
|
| 49 |
+
.mhrn-accel-boundary{padding:9px;border-left:3px solid var(--accent);background:var(--paper)}
|
| 50 |
+
`;
|
| 51 |
+
document.head.append(style);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function panel(id, parent, title, intro, routeTag = null) {
|
| 55 |
+
if (!parent) return null;
|
| 56 |
+
let node = document.getElementById(id);
|
| 57 |
+
if (node) return node;
|
| 58 |
+
node = document.createElement("section");
|
| 59 |
+
node.id = id;
|
| 60 |
+
node.className = "mhrn-accel-panel";
|
| 61 |
+
if (routeTag) node.dataset.mhrnRoute = routeTag;
|
| 62 |
+
node.innerHTML = `
|
| 63 |
+
<div class="mhrn-accel-head">
|
| 64 |
+
<div><span class="workspace-kicker">CUDA · FE-3</span><h3>${escapeHtml(title)}</h3><p>${escapeHtml(intro)}</p></div>
|
| 65 |
+
<button type="button" data-accel-refresh>Aktualisieren</button>
|
| 66 |
+
</div>
|
| 67 |
+
<div data-accel-body>Lade kanonischen Integrationsstatus …</div>
|
| 68 |
+
`;
|
| 69 |
+
parent.append(node);
|
| 70 |
+
return node;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
function ensurePanels() {
|
| 74 |
+
const playgroundHost = document.getElementById("pg-mhrn-integration") || document.getElementById("tab-playground");
|
| 75 |
+
const releaseHost = document.querySelector('[data-release-view="development"]');
|
| 76 |
+
const scienceMetrics = document.getElementById("mhrn-scientific-metrics");
|
| 77 |
+
const scienceHost = scienceMetrics?.parentElement || document.getElementById("tab-research");
|
| 78 |
+
const oldHost = document.querySelector('#tab-old [data-area-overview="old"]') || document.getElementById("tab-old");
|
| 79 |
+
return {
|
| 80 |
+
playground: panel(
|
| 81 |
+
"mhrn-acceleration-playground",
|
| 82 |
+
playgroundHost,
|
| 83 |
+
"Playground → MHRN Integrationsbrücke",
|
| 84 |
+
"Welche Playground-Bausteine bereits kanonisch sind und welches Gate als Nächstes fehlt."
|
| 85 |
+
),
|
| 86 |
+
release: panel(
|
| 87 |
+
"mhrn-acceleration-release",
|
| 88 |
+
releaseHost,
|
| 89 |
+
"CUDA / FE-3 Engineering Acceptance",
|
| 90 |
+
"Release-Sicht auf Backend-Vertrag, FE-3-Manifest und physische Hardware-Abnahme."
|
| 91 |
+
),
|
| 92 |
+
science: panel(
|
| 93 |
+
"mhrn-acceleration-science",
|
| 94 |
+
scienceHost,
|
| 95 |
+
"Backend-Parität: Evidenzgrenze",
|
| 96 |
+
"Engineering Verification bleibt getrennt von wissenschaftlichen DATA/EVID.",
|
| 97 |
+
"science:observatory"
|
| 98 |
+
),
|
| 99 |
+
old: panel(
|
| 100 |
+
"mhrn-acceleration-old",
|
| 101 |
+
oldHost,
|
| 102 |
+
"Archivierte Integrationsflächen",
|
| 103 |
+
"OLD bleibt funktionsfähig, ist aber kein automatischer Promotion-Pfad in den MHRN-Core."
|
| 104 |
+
),
|
| 105 |
+
};
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
function waveMarkup(waves) {
|
| 109 |
+
return `<div class="mhrn-accel-grid">${(waves || []).map((wave) => `
|
| 110 |
+
<article class="mhrn-accel-card" data-accel-wave="${escapeHtml(wave.id)}">
|
| 111 |
+
<h4>${escapeHtml(wave.label)}</h4>
|
| 112 |
+
<span class="mhrn-accel-state ${statusClass(wave.status)}">${escapeHtml(STATUS_LABELS[wave.status] || wave.status)}</span>
|
| 113 |
+
</article>
|
| 114 |
+
`).join("")}</div>`;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
function hardwareMarkup(acceleration) {
|
| 118 |
+
const hardware = acceleration.hardware_acceptance || {};
|
| 119 |
+
const manifest = acceleration.fe3_manifest || {};
|
| 120 |
+
const backend = acceleration.backend || {};
|
| 121 |
+
const caps = backend.capabilities || {};
|
| 122 |
+
const pan = acceleration.pan_wave5b_preflight || {};
|
| 123 |
+
return `
|
| 124 |
+
<div class="mhrn-accel-boundary"><strong>Interpretationsgrenze:</strong> ${escapeHtml(acceleration.provenance_rule)} Keine Anzeige in diesem Panel erzeugt DATA oder EVID.</div>
|
| 125 |
+
${waveMarkup(acceleration.waves)}
|
| 126 |
+
<dl class="mhrn-accel-meta">
|
| 127 |
+
<dt>FE-3 Manifest</dt><dd>${escapeHtml(manifest.status)} · ${escapeHtml(manifest.manifest_sha256 || "—")}</dd>
|
| 128 |
+
<dt>CUDA Backend</dt><dd>${escapeHtml(backend.backend_version || backend.status || "—")}</dd>
|
| 129 |
+
<dt>Live Input</dt><dd>${caps.supports_live_external_input === true ? "ja" : "nein"}</dd>
|
| 130 |
+
<dt>Grenzen</dt><dd>${escapeHtml(caps.max_neurons || "—")} Neuronen · ${escapeHtml(caps.max_edges || "—")} Kanten · ${escapeHtml(caps.max_ticks || "—")} Ticks</dd>
|
| 131 |
+
<dt>Plasticity</dt><dd>${escapeHtml(caps.plasticity_semantics || "—")}</dd>
|
| 132 |
+
<dt>PAN Wave 5B</dt><dd>${pan.preflight_ready === true ? "Preflight bereit" : "Preflight unvollständig"} · ${pan.ready_for_execution === true ? "Ausführung freigegeben" : "Ausführung gesperrt"}</dd>
|
| 133 |
+
<dt>PAN Contract</dt><dd>${escapeHtml(pan.contract_status || "—")} · ${pan.contract_frozen === true ? "frozen" : "nicht frozen"}</dd>
|
| 134 |
+
<dt>PAN Blocker</dt><dd>${escapeHtml((pan.blockers || []).join(", ") || "keine")}</dd>
|
| 135 |
+
<dt>Hardware</dt><dd><span class="mhrn-accel-state ${statusClass(hardware.status)}">${escapeHtml(STATUS_LABELS[hardware.status] || hardware.status || "—")}</span> ${escapeHtml(hardware.gpu_identity || "")}</dd>
|
| 136 |
+
<dt>Artefakt</dt><dd>${escapeHtml(hardware.artifact || "noch kein HARDWARE_ACCEPTANCE_<date>.json")}</dd>
|
| 137 |
+
<dt>Nächstes Gate</dt><dd>${escapeHtml(acceleration.next_gate || "—")}</dd>
|
| 138 |
+
</dl>
|
| 139 |
+
`;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
function scienceMarkup(acceleration) {
|
| 143 |
+
const hardware = acceleration.hardware_acceptance || {};
|
| 144 |
+
return `
|
| 145 |
+
<div class="mhrn-accel-boundary">
|
| 146 |
+
<strong>Kein Evidenzsprung.</strong> FE-3 und CUDA-Parität sind Engineering Verification. Selbst ein grünes physisches Artefakt wird nicht automatisch DATA/EVID.
|
| 147 |
+
</div>
|
| 148 |
+
<div class="mhrn-accel-grid">
|
| 149 |
+
<article class="mhrn-accel-card"><h4>Softwarepfad</h4><span class="mhrn-accel-state ${acceleration.software_path_closed ? "ok" : "pending"}">${acceleration.software_path_closed ? "geschlossen" : "offen"}</span></article>
|
| 150 |
+
<article class="mhrn-accel-card"><h4>Physische FE-3-Abnahme</h4><span class="mhrn-accel-state ${statusClass(hardware.status)}">${escapeHtml(STATUS_LABELS[hardware.status] || hardware.status)}</span></article>
|
| 151 |
+
<article class="mhrn-accel-card"><h4>Scientific status</h4><span class="mhrn-accel-state pending">keine DATA / keine EVID</span></article>
|
| 152 |
+
</div>
|
| 153 |
+
`;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
function oldMarkup(catalog) {
|
| 157 |
+
const old = (catalog?.candidates || []).find((item) => item.element_id === "old_frontend_views");
|
| 158 |
+
const routes = old?.old_routes || [];
|
| 159 |
+
return `
|
| 160 |
+
<div class="mhrn-accel-boundary"><strong>OLD-Regel:</strong> Aufbewahren statt löschen. Reaktivierung erfolgt explizit; Archivansichten werden nicht stillschweigend kanonisch.</div>
|
| 161 |
+
<p><strong>${routes.length}</strong> Integrations-/Kompatibilitätsrouten sind im Promotion-Katalog ausdrücklich als OLD markiert.</p>
|
| 162 |
+
<p>${escapeHtml(routes.join(", ") || "Keine expliziten OLD-Routen gemeldet.")}</p>
|
| 163 |
+
`;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
async function refresh(panels) {
|
| 167 |
+
const [integration, catalog] = await Promise.all([
|
| 168 |
+
readJson("/api/integration/status"),
|
| 169 |
+
readJson("/api/playground/integration"),
|
| 170 |
+
]);
|
| 171 |
+
const acceleration = integration.acceleration;
|
| 172 |
+
if (!acceleration) throw new Error("/api/integration/status enthält keinen acceleration-Block");
|
| 173 |
+
|
| 174 |
+
if (panels.playground) panels.playground.querySelector("[data-accel-body]").innerHTML = hardwareMarkup(acceleration);
|
| 175 |
+
if (panels.release) panels.release.querySelector("[data-accel-body]").innerHTML = hardwareMarkup(acceleration);
|
| 176 |
+
if (panels.science) panels.science.querySelector("[data-accel-body]").innerHTML = scienceMarkup(acceleration);
|
| 177 |
+
if (panels.old) panels.old.querySelector("[data-accel-body]").innerHTML = oldMarkup(catalog);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
export function initAccelerationIntegrationStatus() {
|
| 181 |
+
injectStyles();
|
| 182 |
+
const panels = ensurePanels();
|
| 183 |
+
const roots = Object.values(panels).filter(Boolean);
|
| 184 |
+
if (!roots.length) return;
|
| 185 |
+
|
| 186 |
+
document.addEventListener("click", (event) => {
|
| 187 |
+
const button = event.target.closest("[data-accel-refresh]");
|
| 188 |
+
if (button && roots.some((root) => root.contains(button))) {
|
| 189 |
+
refresh(panels).catch((error) => {
|
| 190 |
+
roots.forEach((root) => {
|
| 191 |
+
const body = root.querySelector("[data-accel-body]");
|
| 192 |
+
if (body) body.textContent = `Integrationsstatus nicht verfügbar: ${error.message || error}`;
|
| 193 |
+
});
|
| 194 |
+
});
|
| 195 |
+
}
|
| 196 |
+
});
|
| 197 |
+
|
| 198 |
+
refresh(panels).catch((error) => {
|
| 199 |
+
roots.forEach((root) => {
|
| 200 |
+
const body = root.querySelector("[data-accel-body]");
|
| 201 |
+
if (body) body.textContent = `Integrationsstatus nicht verfügbar: ${error.message || error}`;
|
| 202 |
+
});
|
| 203 |
+
});
|
| 204 |
+
}
|
src/dashboard/static/frontend/modules/neuron-model-science.js
CHANGED
|
@@ -62,8 +62,8 @@ const FIELD_LABELS = Object.freeze({
|
|
| 62 |
"neuron.enable_homeostasis": "Homeostatic regulation",
|
| 63 |
});
|
| 64 |
|
| 65 |
-
const ROUTE_ID = "cellmodel";
|
| 66 |
-
const ROUTE = Object.freeze([ROUTE_ID, "Cell
|
| 67 |
const ROUTER_STORAGE_KEY = "mhrn-workspace-router-v1";
|
| 68 |
|
| 69 |
const state = {
|
|
@@ -295,25 +295,25 @@ function addContextRouteButton(nav, architecture) {
|
|
| 295 |
button.setAttribute("role", "tab");
|
| 296 |
button.dataset.areaRoute = ROUTE_ID;
|
| 297 |
button.title = "Stage 0 single-neuron model and settings";
|
| 298 |
-
button.textContent = "Cell
|
| 299 |
-
const networkButton = nav.querySelector('[data-area-route="network"]');
|
| 300 |
if (networkButton?.nextSibling) nav.insertBefore(button, networkButton.nextSibling);
|
| 301 |
else nav.appendChild(button);
|
| 302 |
-
button.addEventListener("click", () => architecture.selectRoute("
|
| 303 |
}
|
| 304 |
|
| 305 |
function addOverviewRouteCard(architecture) {
|
| 306 |
-
const grid = document.querySelector('[data-area-overview="
|
| 307 |
if (!grid || grid.querySelector(`[data-route-card="${ROUTE_ID}"]`)) return;
|
| 308 |
const button = document.createElement("button");
|
| 309 |
button.type = "button";
|
| 310 |
button.dataset.routeCard = ROUTE_ID;
|
| 311 |
-
button.title = "Cell
|
| 312 |
-
button.innerHTML = '<span>
|
| 313 |
-
const networkCard = grid.querySelector('[data-route-card="network"]');
|
| 314 |
if (networkCard?.nextSibling) grid.insertBefore(button, networkCard.nextSibling);
|
| 315 |
else grid.appendChild(button);
|
| 316 |
-
button.addEventListener("click", () => architecture.selectRoute("
|
| 317 |
}
|
| 318 |
|
| 319 |
function ensureSettingsNavigationFallback(architecture) {
|
|
@@ -330,15 +330,15 @@ function ensureSettingsNavigationFallback(architecture) {
|
|
| 330 |
|
| 331 |
function ensureCanonicalRoute() {
|
| 332 |
const architecture = window.MHRNWorkspaceArchitecture;
|
| 333 |
-
const routes = architecture?.areas?.
|
| 334 |
if (!architecture?.selectRoute || !Array.isArray(routes)) return false;
|
| 335 |
|
| 336 |
if (!routes.some(([id]) => id === ROUTE_ID)) {
|
| 337 |
-
const networkIndex = routes.findIndex(([id]) => id === "network");
|
| 338 |
routes.splice(networkIndex >= 0 ? networkIndex + 1 : routes.length, 0, [...ROUTE]);
|
| 339 |
}
|
| 340 |
|
| 341 |
-
document.querySelectorAll('.mhrn-context-nav[data-area="
|
| 342 |
addContextRouteButton(nav, architecture);
|
| 343 |
});
|
| 344 |
addOverviewRouteCard(architecture);
|
|
@@ -381,7 +381,7 @@ function ensureWorkbench() {
|
|
| 381 |
</div>
|
| 382 |
<div id="science-neuron-note" class="neuron-model-warning" role="note"></div>
|
| 383 |
<div class="neuron-model-shortcuts" aria-label="Related settings">
|
| 384 |
-
<button type="button" class="btn-secondary" data-route-jump="
|
| 385 |
<button type="button" class="btn-secondary" data-route-jump="settings:overview">App Settings</button>
|
| 386 |
</div>
|
| 387 |
<div id="science-neuron-fields" class="neuron-model-fields" aria-live="polite"></div>
|
|
@@ -419,8 +419,8 @@ function bind() {
|
|
| 419 |
function restoreCellModelRouteIfRequested() {
|
| 420 |
try {
|
| 421 |
const saved = JSON.parse(localStorage.getItem(ROUTER_STORAGE_KEY) || "null");
|
| 422 |
-
if (saved?.area === "
|
| 423 |
-
window.MHRNWorkspaceArchitecture?.selectRoute?.("
|
| 424 |
}
|
| 425 |
} catch (_) {}
|
| 426 |
}
|
|
|
|
| 62 |
"neuron.enable_homeostasis": "Homeostatic regulation",
|
| 63 |
});
|
| 64 |
|
| 65 |
+
const ROUTE_ID = "science-cellmodel";
|
| 66 |
+
const ROUTE = Object.freeze([ROUTE_ID, "Wissenschaft · Cell Modell", "network", "focus", "#mhrn-neuron-model-science"]);
|
| 67 |
const ROUTER_STORAGE_KEY = "mhrn-workspace-router-v1";
|
| 68 |
|
| 69 |
const state = {
|
|
|
|
| 295 |
button.setAttribute("role", "tab");
|
| 296 |
button.dataset.areaRoute = ROUTE_ID;
|
| 297 |
button.title = "Stage 0 single-neuron model and settings";
|
| 298 |
+
button.textContent = "Wissenschaft · Cell Modell";
|
| 299 |
+
const networkButton = nav.querySelector('[data-area-route="science-network"]');
|
| 300 |
if (networkButton?.nextSibling) nav.insertBefore(button, networkButton.nextSibling);
|
| 301 |
else nav.appendChild(button);
|
| 302 |
+
button.addEventListener("click", () => architecture.selectRoute("old", ROUTE_ID));
|
| 303 |
}
|
| 304 |
|
| 305 |
function addOverviewRouteCard(architecture) {
|
| 306 |
+
const grid = document.querySelector('[data-area-overview="old"] .mhrn-area-route-grid');
|
| 307 |
if (!grid || grid.querySelector(`[data-route-card="${ROUTE_ID}"]`)) return;
|
| 308 |
const button = document.createElement("button");
|
| 309 |
button.type = "button";
|
| 310 |
button.dataset.routeCard = ROUTE_ID;
|
| 311 |
+
button.title = "Cell Modell öffnen";
|
| 312 |
+
button.innerHTML = '<span>W0</span><strong>Wissenschaft · Cell Modell</strong>';
|
| 313 |
+
const networkCard = grid.querySelector('[data-route-card="science-network"]');
|
| 314 |
if (networkCard?.nextSibling) grid.insertBefore(button, networkCard.nextSibling);
|
| 315 |
else grid.appendChild(button);
|
| 316 |
+
button.addEventListener("click", () => architecture.selectRoute("old", ROUTE_ID));
|
| 317 |
}
|
| 318 |
|
| 319 |
function ensureSettingsNavigationFallback(architecture) {
|
|
|
|
| 330 |
|
| 331 |
function ensureCanonicalRoute() {
|
| 332 |
const architecture = window.MHRNWorkspaceArchitecture;
|
| 333 |
+
const routes = architecture?.areas?.old?.routes;
|
| 334 |
if (!architecture?.selectRoute || !Array.isArray(routes)) return false;
|
| 335 |
|
| 336 |
if (!routes.some(([id]) => id === ROUTE_ID)) {
|
| 337 |
+
const networkIndex = routes.findIndex(([id]) => id === "science-network");
|
| 338 |
routes.splice(networkIndex >= 0 ? networkIndex + 1 : routes.length, 0, [...ROUTE]);
|
| 339 |
}
|
| 340 |
|
| 341 |
+
document.querySelectorAll('.mhrn-context-nav[data-area="old"]').forEach((nav) => {
|
| 342 |
addContextRouteButton(nav, architecture);
|
| 343 |
});
|
| 344 |
addOverviewRouteCard(architecture);
|
|
|
|
| 381 |
</div>
|
| 382 |
<div id="science-neuron-note" class="neuron-model-warning" role="note"></div>
|
| 383 |
<div class="neuron-model-shortcuts" aria-label="Related settings">
|
| 384 |
+
<button type="button" class="btn-secondary" data-route-jump="settings:parameters">Alle Parameter</button>
|
| 385 |
<button type="button" class="btn-secondary" data-route-jump="settings:overview">App Settings</button>
|
| 386 |
</div>
|
| 387 |
<div id="science-neuron-fields" class="neuron-model-fields" aria-live="polite"></div>
|
|
|
|
| 419 |
function restoreCellModelRouteIfRequested() {
|
| 420 |
try {
|
| 421 |
const saved = JSON.parse(localStorage.getItem(ROUTER_STORAGE_KEY) || "null");
|
| 422 |
+
if (saved?.area === "old" && saved?.route === ROUTE_ID) {
|
| 423 |
+
window.MHRNWorkspaceArchitecture?.selectRoute?.("old", ROUTE_ID);
|
| 424 |
}
|
| 425 |
} catch (_) {}
|
| 426 |
}
|
src/dashboard/static/frontend/modules/playground-workspace.js
ADDED
|
@@ -0,0 +1,859 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Guided controls share the existing Playground form and API contract.
|
| 2 |
+
const $ = (id) => document.getElementById(id);
|
| 3 |
+
const escape = (value) =>
|
| 4 |
+
String(value ?? "").replace(
|
| 5 |
+
/[&<>"']/g,
|
| 6 |
+
(c) =>
|
| 7 |
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
| 8 |
+
c
|
| 9 |
+
],
|
| 10 |
+
);
|
| 11 |
+
const groups = [
|
| 12 |
+
[
|
| 13 |
+
"Modell & Netzwerk",
|
| 14 |
+
"Zellen, Verbindungen und zeitliche Dynamik",
|
| 15 |
+
[1, 2, 3, 15],
|
| 16 |
+
],
|
| 17 |
+
["Lauf & Stimulus", "Eingabe, Seed und Ausführung festlegen", [4, 7]],
|
| 18 |
+
["PAN-Kern", "Hyperzustand, Geometrie und Feedback", [5, 6]],
|
| 19 |
+
[
|
| 20 |
+
"Lernen & Closed Loop",
|
| 21 |
+
"Kanäle, Aktionen, Ziele und Kredit-Zuweisung",
|
| 22 |
+
[8, 10, 11, 12, 13, 14],
|
| 23 |
+
],
|
| 24 |
+
["Anwendung", "Körperhaltung, Reward-Ereignisse und Episoden", [20, 21, 22]],
|
| 25 |
+
[
|
| 26 |
+
"Operationen",
|
| 27 |
+
"Neural I/O, Live-Monitor, CUDA und Nachtlauf",
|
| 28 |
+
[9, 17, 18, 19],
|
| 29 |
+
],
|
| 30 |
+
];
|
| 31 |
+
const channelFields = [
|
| 32 |
+
"pg-input-amplitudes",
|
| 33 |
+
"pg-input-frequencies",
|
| 34 |
+
"pg-input-phases",
|
| 35 |
+
];
|
| 36 |
+
const limits = [
|
| 37 |
+
[0, 500, 0],
|
| 38 |
+
[1, 200, 20],
|
| 39 |
+
[0, Math.PI * 2, 0],
|
| 40 |
+
];
|
| 41 |
+
const help = {
|
| 42 |
+
"pg-pan-feedback-threshold":
|
| 43 |
+
"Schwelle für das konfigurierte Feedback-Routing. Zusammen mit Quelle, Nichtlinearität und Sättigung betrachten.",
|
| 44 |
+
"pg-inhibitory-fraction":
|
| 45 |
+
"Anteil inhibitorischer Neuronen. Ohne GABA-Stärke entsteht daraus keine inhibitorische Wirkung.",
|
| 46 |
+
"pg-gaba-strength":
|
| 47 |
+
"Stärke der inhibitorischen Kopplung; 0 deaktiviert deren Wirkung.",
|
| 48 |
+
"pg-behavior-epsilon":
|
| 49 |
+
"Explorationswahrscheinlichkeit: 0 nutzt die bevorzugte Aktion, 1 exploriert immer.",
|
| 50 |
+
"pg-seed":
|
| 51 |
+
"Ein fester Seed macht gleiche CPU-Konfigurationen reproduzierbar. Modell- und Hardware-Parität separat prüfen.",
|
| 52 |
+
"pg-action-to-input-map":
|
| 53 |
+
"auto erzeugt die Zuordnung im Backend. Alternativ eine JSON-Matrix mit einer Zeile je Aktion eingeben.",
|
| 54 |
+
"pg-threshold-variance":
|
| 55 |
+
"Streuung der Neuronenschwellen. 0 lässt alle Schwellen identisch.",
|
| 56 |
+
"pg-tau-m-variance":
|
| 57 |
+
"Streuung der Membranzeitkonstanten. Mit identischem Seed gegen 0 vergleichen.",
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
export function initGuidedWorkspace({
|
| 61 |
+
payload,
|
| 62 |
+
presets,
|
| 63 |
+
applyPreset,
|
| 64 |
+
setValue,
|
| 65 |
+
lastResult,
|
| 66 |
+
}) {
|
| 67 |
+
const root = $("playground-builder");
|
| 68 |
+
if (!root || root.dataset.guided) return;
|
| 69 |
+
root.dataset.guided = "true";
|
| 70 |
+
const style = document.createElement("style");
|
| 71 |
+
style.textContent = `
|
| 72 |
+
#playground-run .playground-analysis-card{min-width:0}#playground-run .playground-analysis-card pre{max-width:100%;overflow:auto}
|
| 73 |
+
.pg-page-intro{padding:18px 0 12px;border-bottom:1px solid var(--rule);margin-bottom:16px}.pg-page-intro h2{font-size:1.35rem;margin:0 0 6px}.pg-page-intro p{color:var(--ink-3);max-width:75ch;line-height:1.5}
|
| 74 |
+
.pg-group{margin:12px 0;border:1px solid var(--rule-2);border-radius:8px;background:var(--paper-2);overflow:hidden}.pg-group>summary{padding:16px;cursor:pointer;font-size:1rem;font-weight:650}.pg-group>summary small{display:block;font-size:.75rem;font-weight:400;color:var(--ink-3);margin:5px 0 0 18px}.pg-group-body{padding:0 12px 12px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.pg-group-body>*{min-width:0}.pg-group-body>.playground-card{border-top:1px solid var(--rule)}.pg-group-body>.pg-wide{grid-column:1/-1}.pg-section-reset{float:right;font-size:.65rem;padding:3px 7px}
|
| 75 |
+
#tab-playground button{cursor:pointer}#tab-playground button:disabled{opacity:.5;cursor:not-allowed}#tab-playground :focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
| 76 |
+
.pg-tools{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0}.pg-tools button,.pg-page-intro button{min-height:36px;padding:7px 12px}.pg-preset-grid{max-height:360px;overflow:auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin:12px 0}.pg-preset-card{border:1px solid var(--rule);border-radius:6px;padding:14px;background:var(--paper-2);display:flex;flex-direction:column;gap:8px}.pg-preset-card h4,.pg-preset-card p{margin:0}.pg-preset-card p{font-size:.78rem;line-height:1.45}.pg-preset-card small{color:var(--ink-3)}.pg-preset-card button{margin-top:auto;align-self:start}.pg-preset-card[hidden]{display:none}
|
| 77 |
+
.pg-channel-wrap{grid-column:1/-1;overflow:auto}.pg-channel-table{width:100%;border-collapse:collapse;font-size:.8rem}.pg-channel-table th,.pg-channel-table td{padding:8px;border-bottom:1px solid var(--rule);text-align:left}.pg-channel-table input{width:100%;min-width:75px}.pg-channel-table td:nth-child(2) input{min-width:160px}.pg-validation{margin:12px 0;font-size:.8rem;line-height:1.6}.pg-validation a{color:var(--crimson)}.pg-warning{color:var(--amber);padding:4px 0}.pg-field-error{display:block;color:var(--crimson);font-size:.7rem;grid-column:1/-1}[aria-invalid=true]{outline:2px solid var(--crimson)!important}.pg-help{display:block;font-size:.68rem;line-height:1.4;color:var(--ink-3);grid-column:1/-1}
|
| 78 |
+
.pg-run-empty{padding:36px;border:1px dashed var(--rule-3);text-align:center;color:var(--ink-3)}.pg-config-preview{max-height:320px;overflow:auto;white-space:pre-wrap;font-size:.75rem}.pg-search{width:100%;max-width:460px;padding:10px;margin:10px 0}.pg-summary{padding:12px;border-left:3px solid var(--accent);background:var(--paper-2);line-height:1.6;font-size:.8rem}.pg-raw{margin:12px 0}.pg-raw>summary{cursor:pointer;padding:8px;font-size:.8rem}
|
| 79 |
+
@media(max-width:850px){.pg-group-body{grid-template-columns:1fr}.pg-preset-grid{grid-template-columns:1fr}.pg-group>summary{font-size:.9rem}.pg-tools button{min-height:44px}.pg-live-monitor-body{overflow:auto;max-height:75vh}.pg-live-monitor-params{overflow:auto}.playground-session{flex-wrap:wrap}}
|
| 80 |
+
`;
|
| 81 |
+
document.head.append(style);
|
| 82 |
+
const intro = (id, title, description) => {
|
| 83 |
+
const node = document.createElement("header");
|
| 84 |
+
node.className = "pg-page-intro";
|
| 85 |
+
node.innerHTML = `<h2>${title}</h2><p>${description}</p>`;
|
| 86 |
+
$(id).prepend(node);
|
| 87 |
+
return node;
|
| 88 |
+
};
|
| 89 |
+
intro(
|
| 90 |
+
"playground-builder",
|
| 91 |
+
"Experiment aufbauen",
|
| 92 |
+
"1 · Startpunkt wählen. 2 · Parameter prüfen. 3 · CPU, CUDA-Membran oder CUDA-PAN-Zustand wählen und den Lauf starten.",
|
| 93 |
+
);
|
| 94 |
+
intro(
|
| 95 |
+
"playground-run",
|
| 96 |
+
"Lauf & Auswertung",
|
| 97 |
+
"Aktivität, Netzwerk und Lernmechanismen gemeinsam betrachten. Die Diagramme zeigen den zuletzt ausgeführten oder geladenen Playground-Lauf.",
|
| 98 |
+
);
|
| 99 |
+
intro(
|
| 100 |
+
"playground-sessions",
|
| 101 |
+
"Gespeicherte Sessions",
|
| 102 |
+
"Persistierte Läufe vom lokalen Server öffnen und deren Ergebnisse vergleichen. Browser-Presets und Server-Sessions sind getrennte Speicher.",
|
| 103 |
+
);
|
| 104 |
+
intro(
|
| 105 |
+
"playground-catalog",
|
| 106 |
+
"Bausteine & Fähigkeiten",
|
| 107 |
+
"Modelle, Topologien, Lernregeln und Schnittstellen erkunden. Ein Eintrag öffnet die Erklärung und den Implementierungsstatus.",
|
| 108 |
+
);
|
| 109 |
+
const cards = new Map();
|
| 110 |
+
root.querySelectorAll("h3").forEach((h) => {
|
| 111 |
+
const n = Number(h.textContent.match(/^(\d+) ·/)?.[1]);
|
| 112 |
+
if (n) cards.set(n, h.parentElement);
|
| 113 |
+
});
|
| 114 |
+
const container = document.createElement("div");
|
| 115 |
+
container.id = "pg-groups";
|
| 116 |
+
root.querySelector(".playground-grid").before(container);
|
| 117 |
+
root.querySelectorAll("input[type=number][step]").forEach((e) => {
|
| 118 |
+
if (e.step.includes(".")) e.step = "any";
|
| 119 |
+
});
|
| 120 |
+
const defaults = new Map();
|
| 121 |
+
const controls = () =>
|
| 122 |
+
[...root.querySelectorAll("input[id],select[id],textarea[id]")].filter(
|
| 123 |
+
(e) =>
|
| 124 |
+
(e.id === "pg-user-preset-select" || !e.id.startsWith("pg-user-")) &&
|
| 125 |
+
!e.id.startsWith("pg-workspace-"),
|
| 126 |
+
);
|
| 127 |
+
const snapshot = () =>
|
| 128 |
+
Object.fromEntries(
|
| 129 |
+
controls().map((e) => [
|
| 130 |
+
e.id,
|
| 131 |
+
e.type === "checkbox" ? e.checked : e.value,
|
| 132 |
+
]),
|
| 133 |
+
);
|
| 134 |
+
groups.forEach(([title, description, numbers], index) => {
|
| 135 |
+
const group = document.createElement("details");
|
| 136 |
+
group.className = "pg-group";
|
| 137 |
+
group.id = `pg-group-${index}`;
|
| 138 |
+
group.open = index === 0;
|
| 139 |
+
group.innerHTML = `<summary>${String.fromCharCode(65 + index)} · ${title}<small>${description}</small></summary><div class="pg-group-body"></div>`;
|
| 140 |
+
container.append(group);
|
| 141 |
+
numbers.forEach((n, position) => {
|
| 142 |
+
const card = cards.get(n);
|
| 143 |
+
if (!card) return;
|
| 144 |
+
const heading = card.querySelector("h3");
|
| 145 |
+
heading.textContent = heading.textContent.replace(
|
| 146 |
+
/^\d+ ·/,
|
| 147 |
+
`${String.fromCharCode(65 + index)}${position + 1} ·`,
|
| 148 |
+
);
|
| 149 |
+
if (n >= 17) card.classList.add("pg-wide");
|
| 150 |
+
const button = document.createElement("button");
|
| 151 |
+
button.type = "button";
|
| 152 |
+
button.className = "pg-section-reset";
|
| 153 |
+
button.textContent = "Zurücksetzen";
|
| 154 |
+
button.title = "Auf Werte des Startprofils zurücksetzen";
|
| 155 |
+
button.onclick = () => {
|
| 156 |
+
card
|
| 157 |
+
.querySelectorAll("input[id],select[id],textarea[id]")
|
| 158 |
+
.forEach((e) => {
|
| 159 |
+
if (defaults.has(e.id)) {
|
| 160 |
+
if (e.type === "checkbox") e.checked = defaults.get(e.id);
|
| 161 |
+
else e.value = defaults.get(e.id);
|
| 162 |
+
}
|
| 163 |
+
});
|
| 164 |
+
refresh();
|
| 165 |
+
remember();
|
| 166 |
+
};
|
| 167 |
+
heading.append(button);
|
| 168 |
+
group.lastElementChild.append(card);
|
| 169 |
+
});
|
| 170 |
+
});
|
| 171 |
+
root.querySelectorAll(".playground-grid").forEach((e) => {
|
| 172 |
+
if (!e.children.length) e.remove();
|
| 173 |
+
});
|
| 174 |
+
const tools = document.createElement("div");
|
| 175 |
+
tools.className = "pg-tools";
|
| 176 |
+
tools.innerHTML = `<button type="button" id="pg-workspace-expand">Alle öffnen</button><button type="button" id="pg-workspace-collapse">Alle schließen</button><button type="button" id="pg-workspace-undo" disabled>Änderung rückgängig</button><button type="button" id="pg-workspace-export">Konfiguration exportieren</button><button type="button" id="pg-workspace-import">JSON importieren</button><input type="file" id="pg-workspace-file" accept=".json,application/json" hidden>`;
|
| 177 |
+
container.before(tools);
|
| 178 |
+
const message = document.createElement("div");
|
| 179 |
+
message.id = "pg-workspace-message";
|
| 180 |
+
message.setAttribute("role", "status");
|
| 181 |
+
tools.after(message);
|
| 182 |
+
const validation = document.createElement("div");
|
| 183 |
+
validation.id = "pg-validation";
|
| 184 |
+
validation.className = "pg-validation";
|
| 185 |
+
validation.setAttribute("aria-live", "polite");
|
| 186 |
+
container.before(validation);
|
| 187 |
+
const summary = document.createElement("div");
|
| 188 |
+
summary.id = "pg-workspace-summary";
|
| 189 |
+
summary.className = "pg-summary";
|
| 190 |
+
container.before(summary);
|
| 191 |
+
$("pg-workspace-expand").onclick = () =>
|
| 192 |
+
container.querySelectorAll("details").forEach((e) => (e.open = true));
|
| 193 |
+
$("pg-workspace-collapse").onclick = () =>
|
| 194 |
+
container.querySelectorAll("details").forEach((e) => (e.open = false));
|
| 195 |
+
const history = [];
|
| 196 |
+
function remember() {
|
| 197 |
+
const current = snapshot();
|
| 198 |
+
if (JSON.stringify(current) !== JSON.stringify(history.at(-1))) {
|
| 199 |
+
history.push(current);
|
| 200 |
+
if (history.length > 30) history.shift();
|
| 201 |
+
}
|
| 202 |
+
$("pg-workspace-undo").disabled = history.length < 2;
|
| 203 |
+
}
|
| 204 |
+
$("pg-workspace-undo").onclick = () => {
|
| 205 |
+
history.pop();
|
| 206 |
+
Object.entries(history.at(-1) || {}).forEach(([id, v]) => {
|
| 207 |
+
if ($(id).type === "checkbox") $(id).checked = v;
|
| 208 |
+
else $(id).value = v;
|
| 209 |
+
});
|
| 210 |
+
refresh();
|
| 211 |
+
$("pg-workspace-undo").disabled = history.length < 2;
|
| 212 |
+
};
|
| 213 |
+
const download = (data, name) => {
|
| 214 |
+
const url = URL.createObjectURL(
|
| 215 |
+
new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }),
|
| 216 |
+
);
|
| 217 |
+
const a = document.createElement("a");
|
| 218 |
+
a.href = url;
|
| 219 |
+
a.download = name;
|
| 220 |
+
a.click();
|
| 221 |
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
| 222 |
+
};
|
| 223 |
+
$("pg-workspace-export").onclick = () => {
|
| 224 |
+
try {
|
| 225 |
+
if (!validate())
|
| 226 |
+
throw Error("Bitte zuerst die markierten Felder korrigieren.");
|
| 227 |
+
download(
|
| 228 |
+
{
|
| 229 |
+
schema: "mhrn-playground-config-v1",
|
| 230 |
+
settings: payload(),
|
| 231 |
+
controls: snapshot(),
|
| 232 |
+
},
|
| 233 |
+
"playground-config.json",
|
| 234 |
+
);
|
| 235 |
+
message.textContent =
|
| 236 |
+
"Konfiguration exportiert. Eigene Presets liegen nur im Browser-Speicher dieses Profils.";
|
| 237 |
+
} catch (e) {
|
| 238 |
+
message.textContent = e.message;
|
| 239 |
+
}
|
| 240 |
+
};
|
| 241 |
+
$("pg-workspace-import").onclick = () => $("pg-workspace-file").click();
|
| 242 |
+
$("pg-workspace-file").onchange = async (event) => {
|
| 243 |
+
try {
|
| 244 |
+
const file = event.target.files[0];
|
| 245 |
+
if (!file) return;
|
| 246 |
+
if (file.size > 1000000) throw Error("Datei ist größer als 1 MB.");
|
| 247 |
+
const data = JSON.parse(await file.text());
|
| 248 |
+
if (
|
| 249 |
+
data.schema !== "mhrn-playground-config-v1" ||
|
| 250 |
+
!data.settings ||
|
| 251 |
+
typeof data.settings !== "object" ||
|
| 252 |
+
Array.isArray(data.settings)
|
| 253 |
+
)
|
| 254 |
+
throw Error("Eine exportierte Playground-Konfiguration auswählen.");
|
| 255 |
+
const allowed = new Set(Object.keys(payload()));
|
| 256 |
+
const entries = Object.entries(data.settings);
|
| 257 |
+
if (
|
| 258 |
+
entries.some(
|
| 259 |
+
([k, v]) =>
|
| 260 |
+
!allowed.has(k) ||
|
| 261 |
+
v === null ||
|
| 262 |
+
(!["number", "boolean", "string"].includes(typeof v) &&
|
| 263 |
+
!Array.isArray(v)),
|
| 264 |
+
)
|
| 265 |
+
)
|
| 266 |
+
throw Error(
|
| 267 |
+
"Unbekannte Felder oder ungültige Werte in der Konfiguration.",
|
| 268 |
+
);
|
| 269 |
+
if (
|
| 270 |
+
data.settings.closed_loop_preset &&
|
| 271 |
+
[...$("pg-user-preset-select").options].some(
|
| 272 |
+
(o) => o.value === data.settings.closed_loop_preset,
|
| 273 |
+
)
|
| 274 |
+
)
|
| 275 |
+
$("pg-user-preset-select").value = data.settings.closed_loop_preset;
|
| 276 |
+
if (data.controls) {
|
| 277 |
+
const known = new Map(controls().map((e) => [e.id, e]));
|
| 278 |
+
if (
|
| 279 |
+
Object.entries(data.controls).some(
|
| 280 |
+
([id, v]) =>
|
| 281 |
+
!known.has(id) ||
|
| 282 |
+
typeof v !==
|
| 283 |
+
(known.get(id).type === "checkbox" ? "boolean" : "string"),
|
| 284 |
+
)
|
| 285 |
+
)
|
| 286 |
+
throw Error("Ungültige Formularfelder im Export.");
|
| 287 |
+
Object.entries(data.controls).forEach(([id, v]) => {
|
| 288 |
+
const e = known.get(id);
|
| 289 |
+
if (e.type === "checkbox") e.checked = v;
|
| 290 |
+
else e.value = v;
|
| 291 |
+
});
|
| 292 |
+
} else {
|
| 293 |
+
entries.forEach(([k, v]) => setValue(k, v));
|
| 294 |
+
}
|
| 295 |
+
refresh();
|
| 296 |
+
remember();
|
| 297 |
+
message.textContent =
|
| 298 |
+
"Konfiguration importiert. Hinweise und Validierung vor dem Start prüfen.";
|
| 299 |
+
} catch (e) {
|
| 300 |
+
message.textContent = `Import fehlgeschlagen: ${e.message}`;
|
| 301 |
+
} finally {
|
| 302 |
+
event.target.value = "";
|
| 303 |
+
}
|
| 304 |
+
};
|
| 305 |
+
|
| 306 |
+
const presetBox = document.createElement("details");
|
| 307 |
+
presetBox.open = true;
|
| 308 |
+
presetBox.className = "pg-raw";
|
| 309 |
+
presetBox.innerHTML =
|
| 310 |
+
'<summary>Preset-Katalog erkunden & vergleichen</summary><input class="pg-search" id="pg-workspace-preset-search" aria-label="Presets durchsuchen" placeholder="Preset suchen …"><div class="pg-preset-grid" id="pg-preset-grid"></div><pre class="pg-config-preview" id="pg-workspace-comparison" aria-live="polite"></pre>';
|
| 311 |
+
root.querySelector(".playground-preset-deck").after(presetBox);
|
| 312 |
+
const storageNote = document.createElement("p");
|
| 313 |
+
storageNote.className = "pg-help";
|
| 314 |
+
storageNote.textContent =
|
| 315 |
+
"Integrierte und Katalog-Presets kommen aus dieser Serverversion. Eigene Presets werden im localStorage dieses Browserprofils gespeichert. Exportierte JSON-Dateien können auf einem anderen Gerät importiert werden.";
|
| 316 |
+
presetBox.before(storageNote);
|
| 317 |
+
function renderPresets() {
|
| 318 |
+
const all = presets();
|
| 319 |
+
$("pg-preset-grid").innerHTML = Object.entries(all)
|
| 320 |
+
.map(
|
| 321 |
+
([name, p]) =>
|
| 322 |
+
`<article class="pg-preset-card" data-name="${escape(name)}"><h4>${escape(p.label || name)}</h4><small>${p.source === "catalog" ? "Server-Katalog" : p.saved_at ? "Browser · eigenes Preset" : "Integriert"}</small><p>${escape(p.description)}</p>${p.expected_success != null ? `<small>Hypothese: ${escape(p.expected_success)} Erfolgsanteil · kein Messergebnis</small>` : ""}<button type="button" data-apply="${escape(name)}">Anwenden</button><button type="button" data-compare="${escape(name)}">Mit aktueller Konfiguration vergleichen</button></article>`,
|
| 323 |
+
)
|
| 324 |
+
.join("");
|
| 325 |
+
$("pg-preset-grid")
|
| 326 |
+
.querySelectorAll("[data-apply]")
|
| 327 |
+
.forEach(
|
| 328 |
+
(b) =>
|
| 329 |
+
(b.onclick = () => {
|
| 330 |
+
$("pg-user-preset-select").value = b.dataset.apply;
|
| 331 |
+
applyPreset();
|
| 332 |
+
refresh();
|
| 333 |
+
remember();
|
| 334 |
+
}),
|
| 335 |
+
);
|
| 336 |
+
$("pg-preset-grid")
|
| 337 |
+
.querySelectorAll("[data-compare]")
|
| 338 |
+
.forEach(
|
| 339 |
+
(b) =>
|
| 340 |
+
(b.onclick = () => {
|
| 341 |
+
try {
|
| 342 |
+
const current = payload(),
|
| 343 |
+
settings = all[b.dataset.compare].settings || {};
|
| 344 |
+
$("pg-workspace-comparison").textContent =
|
| 345 |
+
Object.entries(settings)
|
| 346 |
+
.filter(
|
| 347 |
+
([k, v]) =>
|
| 348 |
+
JSON.stringify(v) !== JSON.stringify(current[k]),
|
| 349 |
+
)
|
| 350 |
+
.map(
|
| 351 |
+
([k, v]) =>
|
| 352 |
+
`${k}\n Aktuell: ${JSON.stringify(current[k])}\n Preset: ${JSON.stringify(v)}`,
|
| 353 |
+
)
|
| 354 |
+
.join("\n\n") || "Keine abweichenden Einstellungen.";
|
| 355 |
+
} catch (e) {
|
| 356 |
+
message.textContent = e.message;
|
| 357 |
+
}
|
| 358 |
+
}),
|
| 359 |
+
);
|
| 360 |
+
}
|
| 361 |
+
$("pg-workspace-preset-search").oninput = (e) => {
|
| 362 |
+
$("pg-preset-grid")
|
| 363 |
+
.querySelectorAll("article")
|
| 364 |
+
.forEach(
|
| 365 |
+
(card) =>
|
| 366 |
+
(card.hidden = !card.textContent
|
| 367 |
+
.toLowerCase()
|
| 368 |
+
.includes(e.target.value.toLowerCase())),
|
| 369 |
+
);
|
| 370 |
+
};
|
| 371 |
+
const raw = document.createElement("details");
|
| 372 |
+
raw.className = "pg-raw";
|
| 373 |
+
raw.innerHTML = "<summary>Erweitert: Kanalwerte als JSON</summary>";
|
| 374 |
+
const channelCard = $("pg-input-amplitudes").closest("article");
|
| 375 |
+
channelCard.append(raw);
|
| 376 |
+
["pg-input-channel-map", ...channelFields].forEach((id) =>
|
| 377 |
+
raw.append($(id).closest("label")),
|
| 378 |
+
);
|
| 379 |
+
const table = document.createElement("div");
|
| 380 |
+
table.className = "pg-channel-wrap";
|
| 381 |
+
table.innerHTML =
|
| 382 |
+
'<p class="pg-help">Automatische Neuronenzuordnung bleibt im Backend. Eigene IDs kommagetrennt eingeben. Leere Werte verwenden die Runtime-Defaults: 0 / 20 Hz / 0 rad; kurze Listen werden zyklisch wiederholt.</p><table class="pg-channel-table"><thead><tr><th>Kanal</th><th>Neuronen</th><th>Amplitude</th><th>Hz</th><th>Phase (rad)</th></tr></thead><tbody id="pg-channel-rows"></tbody></table><div class="pg-tools"><button type="button" id="pg-channel-frequency-up">Alle Frequenzen +2 Hz</button><button type="button" id="pg-channel-auto">Neuronenzuordnung automatisch</button></div>';
|
| 383 |
+
raw.before(table);
|
| 384 |
+
const arrays = () =>
|
| 385 |
+
["pg-input-channel-map", ...channelFields].map((id) => {
|
| 386 |
+
const v = JSON.parse($(id).value);
|
| 387 |
+
if (!Array.isArray(v)) throw Error(`${id}: JSON-Liste erwartet.`);
|
| 388 |
+
return v;
|
| 389 |
+
});
|
| 390 |
+
function renderChannels() {
|
| 391 |
+
try {
|
| 392 |
+
const [map, ...vectors] = arrays();
|
| 393 |
+
const n = Number($("pg-input-channels").value);
|
| 394 |
+
if (!Number.isInteger(n) || n < 1 || n > 64) return;
|
| 395 |
+
$("pg-channel-rows").innerHTML = Array.from(
|
| 396 |
+
{ length: n },
|
| 397 |
+
(_, i) =>
|
| 398 |
+
`<tr><th scope="row">${i}</th><td><input id="pg-workspace-channel-map-${i}" aria-label="Kanal ${i} Neuronen" data-channel="${i}" data-column="map" value="${escape(map[i]?.join(", ") || "")}" placeholder="Automatisch · ${escape($("pg-input-topology").value)}"></td>${vectors.map((v, j) => `<td><input id="pg-workspace-channel-${i}-${j}" type="number" required step="any" min="${limits[j][0]}" max="${limits[j][1]}" aria-label="Kanal ${i} ${["Amplitude", "Frequenz", "Phase"][j]}" data-channel="${i}" data-column="${j}" value="${escape(v.length ? v[i % v.length] : limits[j][2])}"></td>`).join("")}</tr>`,
|
| 399 |
+
).join("");
|
| 400 |
+
} catch {
|
| 401 |
+
/* Raw JSON errors are displayed by validation. */
|
| 402 |
+
}
|
| 403 |
+
}
|
| 404 |
+
table.addEventListener("input", (e) => {
|
| 405 |
+
const input = e.target;
|
| 406 |
+
if (!input.dataset.channel) return;
|
| 407 |
+
const i = Number(input.dataset.channel),
|
| 408 |
+
col = input.dataset.column,
|
| 409 |
+
n = Number($("pg-input-channels").value);
|
| 410 |
+
try {
|
| 411 |
+
const [map, ...vectors] = arrays();
|
| 412 |
+
if (col === "map") {
|
| 413 |
+
if (
|
| 414 |
+
input.value.trim() &&
|
| 415 |
+
!/^\d+(\s*,\s*\d+)*$/.test(input.value.trim())
|
| 416 |
+
) {
|
| 417 |
+
input.setCustomValidity(
|
| 418 |
+
"IDs als kommagetrennte ganze Zahlen eingeben.",
|
| 419 |
+
);
|
| 420 |
+
return;
|
| 421 |
+
}
|
| 422 |
+
input.setCustomValidity("");
|
| 423 |
+
while (map.length < n) map.push([]);
|
| 424 |
+
map[i] = input.value.trim() ? input.value.split(",").map(Number) : [];
|
| 425 |
+
$("pg-input-channel-map").value = JSON.stringify(map);
|
| 426 |
+
} else {
|
| 427 |
+
const j = Number(col),
|
| 428 |
+
old = vectors[j];
|
| 429 |
+
const values = Array.from({ length: n }, (_, k) =>
|
| 430 |
+
old.length ? old[k % old.length] : limits[j][2],
|
| 431 |
+
);
|
| 432 |
+
if (input.value !== "" && Number.isFinite(input.valueAsNumber)) {
|
| 433 |
+
values[i] = input.valueAsNumber;
|
| 434 |
+
$(channelFields[j]).value = JSON.stringify(values);
|
| 435 |
+
}
|
| 436 |
+
}
|
| 437 |
+
} catch (e) {
|
| 438 |
+
message.textContent = e.message;
|
| 439 |
+
}
|
| 440 |
+
});
|
| 441 |
+
$("pg-channel-frequency-up").onclick = () => {
|
| 442 |
+
try {
|
| 443 |
+
const v = arrays()[2],
|
| 444 |
+
n = Number($("pg-input-channels").value);
|
| 445 |
+
$("pg-input-frequencies").value = JSON.stringify(
|
| 446 |
+
Array.from({ length: n }, (_, i) =>
|
| 447 |
+
Math.min(200, (v.length ? v[i % v.length] : 20) + 2),
|
| 448 |
+
),
|
| 449 |
+
);
|
| 450 |
+
refresh();
|
| 451 |
+
remember();
|
| 452 |
+
} catch (e) {
|
| 453 |
+
message.textContent = e.message;
|
| 454 |
+
}
|
| 455 |
+
};
|
| 456 |
+
$("pg-channel-auto").onclick = () => {
|
| 457 |
+
$("pg-input-channel-map").value = "[]";
|
| 458 |
+
refresh();
|
| 459 |
+
remember();
|
| 460 |
+
};
|
| 461 |
+
for (const [id, text] of Object.entries(help)) {
|
| 462 |
+
const field = $(id);
|
| 463 |
+
if (field) {
|
| 464 |
+
const note = document.createElement("small");
|
| 465 |
+
note.className = "pg-help";
|
| 466 |
+
note.id = `${id}-help`;
|
| 467 |
+
note.textContent = text;
|
| 468 |
+
field.closest("label").append(note);
|
| 469 |
+
field.setAttribute("aria-describedby", note.id);
|
| 470 |
+
}
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
function validate() {
|
| 474 |
+
const errors = [],
|
| 475 |
+
warnings = [];
|
| 476 |
+
root
|
| 477 |
+
.querySelectorAll("[aria-invalid]")
|
| 478 |
+
.forEach((e) => e.removeAttribute("aria-invalid"));
|
| 479 |
+
root.querySelectorAll(".pg-field-error").forEach((e) => e.remove());
|
| 480 |
+
root.querySelectorAll("input[type=number]").forEach((e) => {
|
| 481 |
+
e.setAttribute(
|
| 482 |
+
"aria-invalid",
|
| 483 |
+
String(
|
| 484 |
+
!e.validity.valid ||
|
| 485 |
+
e.value === "" ||
|
| 486 |
+
!Number.isFinite(e.valueAsNumber),
|
| 487 |
+
),
|
| 488 |
+
);
|
| 489 |
+
if (e.getAttribute("aria-invalid") === "true")
|
| 490 |
+
errors.push([
|
| 491 |
+
e.id,
|
| 492 |
+
e.getAttribute("aria-label") ||
|
| 493 |
+
e.closest("label")?.firstChild?.textContent ||
|
| 494 |
+
"Kanalwert",
|
| 495 |
+
e.validationMessage || "Eine endliche Zahl eingeben.",
|
| 496 |
+
]);
|
| 497 |
+
});
|
| 498 |
+
root.querySelectorAll("[data-column=map]").forEach((e) => {
|
| 499 |
+
if (!e.validity.valid)
|
| 500 |
+
errors.push([e.id, "Neuronenzuordnung", e.validationMessage]);
|
| 501 |
+
});
|
| 502 |
+
const value = (id) => Number($(id)?.value),
|
| 503 |
+
enabled = (id) => $(id)?.checked;
|
| 504 |
+
try {
|
| 505 |
+
const [map, ...vectors] = arrays(),
|
| 506 |
+
n = value("pg-input-channels");
|
| 507 |
+
vectors.forEach((v, j) => {
|
| 508 |
+
if (
|
| 509 |
+
v.length > n ||
|
| 510 |
+
v.some(
|
| 511 |
+
(x) =>
|
| 512 |
+
typeof x !== "number" ||
|
| 513 |
+
!Number.isFinite(x) ||
|
| 514 |
+
x < limits[j][0] ||
|
| 515 |
+
x > limits[j][1],
|
| 516 |
+
)
|
| 517 |
+
)
|
| 518 |
+
errors.push([
|
| 519 |
+
channelFields[j],
|
| 520 |
+
"Input-Kanäle",
|
| 521 |
+
`Maximal ${n} Werte im Bereich ${limits[j][0]} bis ${limits[j][1]}.`,
|
| 522 |
+
]);
|
| 523 |
+
});
|
| 524 |
+
if (
|
| 525 |
+
map.length > n ||
|
| 526 |
+
map.some(
|
| 527 |
+
(row) =>
|
| 528 |
+
!Array.isArray(row) ||
|
| 529 |
+
row.some(
|
| 530 |
+
(x) => !Number.isInteger(x) || x < 0 || x >= value("pg-neurons"),
|
| 531 |
+
),
|
| 532 |
+
)
|
| 533 |
+
)
|
| 534 |
+
errors.push([
|
| 535 |
+
"pg-input-channel-map",
|
| 536 |
+
"Neuronenzuordnung",
|
| 537 |
+
"Nur gültige Neuronen-IDs und maximal eine Gruppe je Kanal verwenden.",
|
| 538 |
+
]);
|
| 539 |
+
if (map.length)
|
| 540 |
+
warnings.push(
|
| 541 |
+
"Eigene Neuronenzuordnung aktiv: leere Zeilen enthalten keine Neuronen. Mit ‚automatisch‘ zur Topologie-Zuordnung zurückkehren.",
|
| 542 |
+
);
|
| 543 |
+
} catch (e) {
|
| 544 |
+
errors.push(["pg-input-amplitudes", "Input-Kanäle", e.message]);
|
| 545 |
+
}
|
| 546 |
+
for (const id of [
|
| 547 |
+
"pg-target-cue-channel",
|
| 548 |
+
"pg-reward-cue-channel",
|
| 549 |
+
"pg-reward-channel",
|
| 550 |
+
"pg-action-feedback-channel",
|
| 551 |
+
"pg-posture-score-channel",
|
| 552 |
+
"pg-reward-event-channel",
|
| 553 |
+
]) {
|
| 554 |
+
if (value(id) >= value("pg-input-channels"))
|
| 555 |
+
errors.push([
|
| 556 |
+
id,
|
| 557 |
+
"Kanalzuordnung",
|
| 558 |
+
`Kanal muss kleiner als ${value("pg-input-channels")} sein.`,
|
| 559 |
+
]);
|
| 560 |
+
}
|
| 561 |
+
if (enabled("pg-posture-reward-enabled") && !enabled("pg-sandbox-enabled"))
|
| 562 |
+
warnings.push(
|
| 563 |
+
"Posture Reward ist aktiv, aber die Stick-Figure Sandbox ist nicht gekoppelt.",
|
| 564 |
+
);
|
| 565 |
+
if (value("pg-inhibitory-fraction") > 0 && value("pg-gaba-strength") === 0)
|
| 566 |
+
warnings.push(
|
| 567 |
+
"Inhibitorischer Anteil ohne GABA-Stärke: die inhibitorische Kopplung bleibt wirkungslos.",
|
| 568 |
+
);
|
| 569 |
+
if (
|
| 570 |
+
enabled("pg-action-loop-enabled") &&
|
| 571 |
+
!$("pg-action-to-input-map").value.trim()
|
| 572 |
+
)
|
| 573 |
+
errors.push([
|
| 574 |
+
"pg-action-to-input-map",
|
| 575 |
+
"Aktions-Loop",
|
| 576 |
+
"auto oder eine gültige Action → Input Map eingeben.",
|
| 577 |
+
]);
|
| 578 |
+
if (
|
| 579 |
+
enabled("pg-reward-enabled") &&
|
| 580 |
+
$("pg-target-encoding").value !== "none" &&
|
| 581 |
+
value("pg-target-cue-channel") === value("pg-reward-channel")
|
| 582 |
+
)
|
| 583 |
+
warnings.push(
|
| 584 |
+
"Ziel und Reward verwenden denselben Kanal. Signale können sich überlagern.",
|
| 585 |
+
);
|
| 586 |
+
if (
|
| 587 |
+
enabled("pg-action-loop-enabled") &&
|
| 588 |
+
$("pg-target-encoding").value !== "none" &&
|
| 589 |
+
value("pg-action-feedback-channel") === value("pg-target-cue-channel")
|
| 590 |
+
)
|
| 591 |
+
warnings.push("Action-Feedback und Ziel-Cue verwenden denselben Kanal.");
|
| 592 |
+
if (value("pg-execution-low") >= value("pg-execution-high"))
|
| 593 |
+
errors.push([
|
| 594 |
+
"pg-execution-low",
|
| 595 |
+
"PAN Runtime",
|
| 596 |
+
"θ low muss kleiner als θ high sein.",
|
| 597 |
+
]);
|
| 598 |
+
if (!errors.length) {
|
| 599 |
+
try {
|
| 600 |
+
payload();
|
| 601 |
+
} catch (e) {
|
| 602 |
+
errors.push(["pg-action-to-input-map", "Konfiguration", e.message]);
|
| 603 |
+
}
|
| 604 |
+
}
|
| 605 |
+
errors.forEach(([id, , text]) => {
|
| 606 |
+
const field = $(id);
|
| 607 |
+
if (field) {
|
| 608 |
+
field.setAttribute("aria-invalid", "true");
|
| 609 |
+
const note = document.createElement("small");
|
| 610 |
+
note.className = "pg-field-error";
|
| 611 |
+
note.textContent = text;
|
| 612 |
+
field.after(note);
|
| 613 |
+
}
|
| 614 |
+
});
|
| 615 |
+
validation.innerHTML =
|
| 616 |
+
errors
|
| 617 |
+
.map(
|
| 618 |
+
([id, label, text]) =>
|
| 619 |
+
`<div><a href="#${id}">${escape(label)}: ${escape(text)}</a></div>`,
|
| 620 |
+
)
|
| 621 |
+
.join("") +
|
| 622 |
+
warnings
|
| 623 |
+
.map((text) => `<div class="pg-warning">Hinweis: ${escape(text)}</div>`)
|
| 624 |
+
.join("");
|
| 625 |
+
for (const id of ["pg-run", "pg-robustness", "pg-user-preset-save"]) {
|
| 626 |
+
if ($(id)) $(id).disabled = errors.length > 0;
|
| 627 |
+
}
|
| 628 |
+
summary.textContent = `${value("pg-neurons")} Neuronen · ${value("pg-edges")} Kanten · ${value("pg-ticks")} Ticks · Seed ${$("pg-seed").value} · ${$("pg-neuron-backend")?.value === "cuda_pan" ? "CUDA-Membran + PAN-Zustand" : $("pg-neuron-backend")?.value === "cuda_membrane" ? "CUDA-Membran / CPU-PAN" : "CPU-Referenz"}. ${errors.length ? `${errors.length} Eingabefehler.` : "Eingaben geprüft."} Laufzeit: noch keine belastbare Messung für diese Konfiguration. CUDA-PAN führt auch PAN-Zustand, Feedback und synaptische Regeln auf der GPU aus. Körper, Policy und übergeordnete Steuerung bleiben CPU; die Ergebnistabelle zeigt die tatsächlichen Komponenten.`;
|
| 629 |
+
return errors.length === 0;
|
| 630 |
+
}
|
| 631 |
+
validation.onclick = (e) => {
|
| 632 |
+
const a = e.target.closest("a");
|
| 633 |
+
if (a) {
|
| 634 |
+
e.preventDefault();
|
| 635 |
+
const target = $(a.hash.slice(1));
|
| 636 |
+
for (
|
| 637 |
+
let parent = target?.parentElement;
|
| 638 |
+
parent;
|
| 639 |
+
parent = parent.parentElement
|
| 640 |
+
)
|
| 641 |
+
if (parent.tagName === "DETAILS") parent.open = true;
|
| 642 |
+
target?.focus();
|
| 643 |
+
}
|
| 644 |
+
};
|
| 645 |
+
function refresh() {
|
| 646 |
+
renderChannels();
|
| 647 |
+
validate();
|
| 648 |
+
}
|
| 649 |
+
root.addEventListener("input", validate);
|
| 650 |
+
root.addEventListener(
|
| 651 |
+
"click",
|
| 652 |
+
(e) => {
|
| 653 |
+
if (
|
| 654 |
+
e.target.closest(
|
| 655 |
+
"#pg-run,#pg-robustness,#pg-live-open,#pg-night-start,#pg-cpu-determinism,#pg-cuda-compile,#pg-cuda-preflight,#pg-cuda-smoke,#pg-cuda-rng",
|
| 656 |
+
) &&
|
| 657 |
+
!validate()
|
| 658 |
+
) {
|
| 659 |
+
e.preventDefault();
|
| 660 |
+
e.stopImmediatePropagation();
|
| 661 |
+
validation.scrollIntoView({ block: "center" });
|
| 662 |
+
}
|
| 663 |
+
},
|
| 664 |
+
true,
|
| 665 |
+
);
|
| 666 |
+
root.addEventListener("change", (e) => {
|
| 667 |
+
if (
|
| 668 |
+
[
|
| 669 |
+
"pg-input-channels",
|
| 670 |
+
"pg-input-topology",
|
| 671 |
+
"pg-neurons",
|
| 672 |
+
...channelFields,
|
| 673 |
+
"pg-input-channel-map",
|
| 674 |
+
].includes(e.target.id)
|
| 675 |
+
) {
|
| 676 |
+
if (e.target.id === "pg-input-channels") {
|
| 677 |
+
const n = Number(e.target.value);
|
| 678 |
+
if (Number.isInteger(n) && n >= 1 && n <= 64) {
|
| 679 |
+
try {
|
| 680 |
+
["pg-input-channel-map", ...channelFields].forEach(
|
| 681 |
+
(id) =>
|
| 682 |
+
($(id).value = JSON.stringify(
|
| 683 |
+
JSON.parse($(id).value).slice(0, n),
|
| 684 |
+
)),
|
| 685 |
+
);
|
| 686 |
+
} catch {}
|
| 687 |
+
}
|
| 688 |
+
}
|
| 689 |
+
refresh();
|
| 690 |
+
}
|
| 691 |
+
remember();
|
| 692 |
+
});
|
| 693 |
+
root.addEventListener("click", (e) => {
|
| 694 |
+
if (
|
| 695 |
+
e.target.closest(
|
| 696 |
+
"#pg-user-preset-apply,#pg-reset,#pg-user-preset-save,#pg-user-preset-delete",
|
| 697 |
+
)
|
| 698 |
+
) {
|
| 699 |
+
refresh();
|
| 700 |
+
renderPresets();
|
| 701 |
+
remember();
|
| 702 |
+
}
|
| 703 |
+
});
|
| 704 |
+
$("pg-user-preset-select").addEventListener("change", () => {
|
| 705 |
+
refresh();
|
| 706 |
+
remember();
|
| 707 |
+
});
|
| 708 |
+
controls().forEach((e) =>
|
| 709 |
+
defaults.set(e.id, e.type === "checkbox" ? e.checked : e.value),
|
| 710 |
+
);
|
| 711 |
+
renderPresets();
|
| 712 |
+
refresh();
|
| 713 |
+
remember();
|
| 714 |
+
|
| 715 |
+
for (const id of [
|
| 716 |
+
"pg-run-json",
|
| 717 |
+
"pg-cuda-stage-state",
|
| 718 |
+
"pg-cuda-resource-state",
|
| 719 |
+
"pg-cuda-parity-state",
|
| 720 |
+
"pg-cuda-rng-state",
|
| 721 |
+
"pg-cuda-compiler-state",
|
| 722 |
+
]) {
|
| 723 |
+
const node = $(id);
|
| 724 |
+
if (!node) continue;
|
| 725 |
+
const detail = document.createElement("details");
|
| 726 |
+
detail.className = "pg-raw";
|
| 727 |
+
detail.innerHTML = "<summary>Technische Details / JSON</summary>";
|
| 728 |
+
node.before(detail);
|
| 729 |
+
detail.append(node);
|
| 730 |
+
if (id.startsWith("pg-cuda-")) {
|
| 731 |
+
const status = document.createElement("p");
|
| 732 |
+
status.className = "pg-summary";
|
| 733 |
+
status.setAttribute("role", "status");
|
| 734 |
+
detail.before(status);
|
| 735 |
+
const update = () => {
|
| 736 |
+
try {
|
| 737 |
+
const data = JSON.parse(node.textContent);
|
| 738 |
+
if ("passed" in data)
|
| 739 |
+
status.textContent = `${data.passed ? "Bestanden" : "Nicht bestanden"}${data.parity ? ` · D2-Abweichung ${data.parity.current_max_abs_error ?? "nicht bestimmbar"} · Wiederholung ${data.gpu_repeat_exact ? "identisch" : "abweichend"}` : ""}${data.samples ? ` · ${data.samples} RNG-Aktionen` : ""}`;
|
| 740 |
+
else if ("ptxas_available" in data)
|
| 741 |
+
status.textContent = `ptxas ${data.ptxas_available ? "verfügbar" : "fehlt"} · Treiber ${data.cuda_driver_available ? "verfügbar" : "fehlt"}. Hardware-Nachweis nur durch einen aktuellen Smoke-Test.`;
|
| 742 |
+
else if (data.ptxas)
|
| 743 |
+
status.textContent = `Register ${data.ptxas.registers ?? "—"} · Spill Store/Load ${data.ptxas.spill_store_bytes ?? "—"}/${data.ptxas.spill_load_bytes ?? "—"} Byte${"ready_for_cooperative_launch" in data ? ` · Preflight ${data.ready_for_cooperative_launch ? "bereit" : "nicht bereit"}` : ""}`;
|
| 744 |
+
else if (data.manifest)
|
| 745 |
+
status.textContent =
|
| 746 |
+
"Gate-IR und PTX erzeugt. Nächster Schritt: ptxas + Occupancy.";
|
| 747 |
+
else
|
| 748 |
+
status.textContent =
|
| 749 |
+
"Diagnose verfügbar – technische Details öffnen.";
|
| 750 |
+
} catch {
|
| 751 |
+
status.textContent = node.textContent;
|
| 752 |
+
}
|
| 753 |
+
};
|
| 754 |
+
new MutationObserver(update).observe(node, { childList: true });
|
| 755 |
+
update();
|
| 756 |
+
}
|
| 757 |
+
}
|
| 758 |
+
const runSummary = document.createElement("div");
|
| 759 |
+
runSummary.id = "pg-result-summary";
|
| 760 |
+
runSummary.className = "pg-run-empty";
|
| 761 |
+
runSummary.textContent =
|
| 762 |
+
"Noch kein Lauf. Im Builder ein Preset wählen und starten oder eine gespeicherte Session öffnen.";
|
| 763 |
+
$("pg-metrics").before(runSummary);
|
| 764 |
+
const backendTable = document.createElement("table");
|
| 765 |
+
backendTable.id = "pg-backend-components";
|
| 766 |
+
backendTable.hidden = true;
|
| 767 |
+
runSummary.after(backendTable);
|
| 768 |
+
const resultPanels = [
|
| 769 |
+
...$("playground-run").querySelectorAll(
|
| 770 |
+
".playground-metrics,.playground-viz-grid,.playground-analysis-grid",
|
| 771 |
+
),
|
| 772 |
+
];
|
| 773 |
+
resultPanels.forEach((panel) => (panel.hidden = true));
|
| 774 |
+
new MutationObserver(() => {
|
| 775 |
+
const r = lastResult();
|
| 776 |
+
resultPanels.forEach((panel) => (panel.hidden = !r?.monitors));
|
| 777 |
+
if (r) {
|
| 778 |
+
backendTable.replaceChildren();
|
| 779 |
+
backendTable.hidden = !r.execution?.components;
|
| 780 |
+
if (r.execution?.components) {
|
| 781 |
+
const caption = document.createElement("caption");
|
| 782 |
+
caption.textContent =
|
| 783 |
+
"Tatsächliche Ausführung dieses Laufs · hybrider Playground, kein vollständiges CUDA-MHRN";
|
| 784 |
+
backendTable.append(caption);
|
| 785 |
+
for (const [component, backend] of Object.entries(
|
| 786 |
+
r.execution.components,
|
| 787 |
+
)) {
|
| 788 |
+
const row = document.createElement("tr"),
|
| 789 |
+
label = document.createElement("th"),
|
| 790 |
+
value = document.createElement("td");
|
| 791 |
+
label.textContent = component.replaceAll("_", " ");
|
| 792 |
+
value.textContent = backend === "cuda" ? "GPU (CUDA)" : "CPU";
|
| 793 |
+
row.append(label, value);
|
| 794 |
+
backendTable.append(row);
|
| 795 |
+
}
|
| 796 |
+
}
|
| 797 |
+
runSummary.className = "pg-summary";
|
| 798 |
+
runSummary.textContent = !r.session_id
|
| 799 |
+
? "Robustheitskontrollen abgeschlossen. Ergebnisse stehen in den technischen Details und im Export bereit."
|
| 800 |
+
: `Session ${r.session_id || "—"} · Seed ${r.config?.seed ?? "—"} · ${r.execution?.neuron_backend === "cuda_pan" ? "CUDA-Membran + PAN-Zustand" : r.execution?.neuron_backend === "cuda_membrane" ? "CUDA-Membran / CPU-PAN" : "CPU-Referenz"} · ${r.metrics?.total_spikes ?? 0} Spikes · explorativer Lauf, keine wissenschaftliche Evidenz.`;
|
| 801 |
+
if (r.cue_decoding?.status === "DESCRIPTIVE_ONLY") {
|
| 802 |
+
runSummary.textContent += ` Aktivitaetsdecoder: ${(r.cue_decoding.accuracy * 100).toFixed(1)} % auf ${r.cue_decoding.test_episodes} Testepisoden; ${r.cue_decoding.input_cue_control || "aligned"}. Kein Nachweis eines neuronalen Lernvorteils.`;
|
| 803 |
+
}
|
| 804 |
+
if (r.behavioral_learning) {
|
| 805 |
+
const learning = r.behavioral_learning;
|
| 806 |
+
const contexts = Object.keys(learning.context_policies || {}).length;
|
| 807 |
+
runSummary.textContent += ` Lernen: ${contexts} Reiz-Kontexte · ${learning.policy_updates || 0} Updates · ${((learning.success_fraction || 0) * 100).toFixed(1)} % Erfolg bei bewerteten Aktionen.`;
|
| 808 |
+
if (r.closed_loop?.policy_context_source) {
|
| 809 |
+
runSummary.textContent +=
|
| 810 |
+
" Kontext aus ausgesendetem Zielreiz; kein Nachweis neuronaler Reizdekodierung.";
|
| 811 |
+
}
|
| 812 |
+
}
|
| 813 |
+
}
|
| 814 |
+
}).observe($("pg-run-json"), { childList: true });
|
| 815 |
+
const runTools = document.createElement("div");
|
| 816 |
+
runTools.className = "pg-tools";
|
| 817 |
+
runTools.innerHTML =
|
| 818 |
+
'<button type="button" id="pg-result-export">Letzten Lauf exportieren</button>';
|
| 819 |
+
runSummary.after(runTools);
|
| 820 |
+
$("pg-result-export").onclick = () => {
|
| 821 |
+
const r = lastResult();
|
| 822 |
+
if (r) download(r, "playground-result.json");
|
| 823 |
+
else
|
| 824 |
+
runSummary.textContent =
|
| 825 |
+
"Zuerst einen Lauf ausführen oder eine Session laden.";
|
| 826 |
+
};
|
| 827 |
+
for (const [id, target, selector, placeholder] of [
|
| 828 |
+
[
|
| 829 |
+
"pg-workspace-session-search",
|
| 830 |
+
"pg-session-list",
|
| 831 |
+
"article",
|
| 832 |
+
"Sessions durchsuchen …",
|
| 833 |
+
],
|
| 834 |
+
[
|
| 835 |
+
"pg-workspace-catalog-search",
|
| 836 |
+
"pg-catalog-grid",
|
| 837 |
+
"article",
|
| 838 |
+
"Bausteine durchsuchen …",
|
| 839 |
+
],
|
| 840 |
+
]) {
|
| 841 |
+
const search = document.createElement("input");
|
| 842 |
+
search.id = id;
|
| 843 |
+
search.className = "pg-search";
|
| 844 |
+
search.placeholder = placeholder;
|
| 845 |
+
search.setAttribute("aria-label", placeholder);
|
| 846 |
+
$(target).before(search);
|
| 847 |
+
const filter = () =>
|
| 848 |
+
$(target)
|
| 849 |
+
.querySelectorAll(selector)
|
| 850 |
+
.forEach(
|
| 851 |
+
(e) =>
|
| 852 |
+
(e.hidden = !e.textContent
|
| 853 |
+
.toLowerCase()
|
| 854 |
+
.includes(search.value.toLowerCase())),
|
| 855 |
+
);
|
| 856 |
+
search.oninput = filter;
|
| 857 |
+
new MutationObserver(filter).observe($(target), { childList: true });
|
| 858 |
+
}
|
| 859 |
+
}
|
src/dashboard/static/frontend/modules/playground.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"use strict";
|
| 2 |
|
| 3 |
import { apiGet, apiPost, byId } from "../core/api.js";
|
|
|
|
| 4 |
import { getLanguage } from "../core/i18n.js?v=i18n-fix-20260920b";
|
| 5 |
|
| 6 |
let lastResult = null;
|
|
@@ -11,7 +12,7 @@ let nightPollTimer = null;
|
|
| 11 |
let catalogInfoItems = new Map();
|
| 12 |
let activeCatalogInfo = null;
|
| 13 |
const PLAYGROUND_PRESETS_STORAGE = "mhrn.playground.presets.v1";
|
| 14 |
-
const DEFAULT_PLAYGROUND_PRESET = "
|
| 15 |
const BUILTIN_PLAYGROUND_PRESETS = {
|
| 16 |
izhikevich_reference: {
|
| 17 |
label: "Izhikevich · Referenz",
|
|
@@ -123,6 +124,7 @@ function updateDefaultHints() {
|
|
| 123 |
field.title = message;
|
| 124 |
field.closest("label")?.classList.toggle("pg-non-default", changed);
|
| 125 |
});
|
|
|
|
| 126 |
}
|
| 127 |
|
| 128 |
function injectStyles() {
|
|
@@ -142,7 +144,7 @@ function injectStyles() {
|
|
| 142 |
.playground-status{padding:9px 11px;border:1px solid var(--rule);border-left:3px solid var(--rule-3);border-radius:var(--r-xs);background:var(--paper-2);color:var(--ink-2);font-size:.68rem;white-space:pre-wrap;overflow:auto}.playground-status[data-state="error"]{border-left-color:var(--crimson);color:var(--crimson)}.playground-status[data-state="ok"]{border-left-color:var(--moss);color:var(--moss)}
|
| 143 |
.pg-live-monitor{width:min(94vw,1000px);max-height:84vh;padding:0;border:1px solid var(--rule-3);border-radius:var(--r-md);background:var(--paper);color:var(--ink);box-shadow:var(--shadow-float)}.pg-live-monitor::backdrop{background:rgba(20,16,12,.55);backdrop-filter:blur(4px)}.pg-live-monitor>header{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:9px 12px;border-bottom:1px solid var(--rule);background:var(--paper-2)}.pg-live-monitor>header h2{margin:0;font-size:.95rem}.pg-live-monitor>header p{margin:2px 0 0;color:var(--ink-3);font-size:.61rem}.pg-live-monitor-close{min-width:28px;padding:0}.pg-live-monitor-body{padding:9px;overflow:hidden}.pg-live-monitor-metrics{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px;margin-bottom:7px}.pg-live-monitor-metric{padding:6px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.pg-live-monitor-metric span{display:block;color:var(--ink-4);font:700 .46rem/1.1 var(--font-mono);text-transform:uppercase}.pg-live-monitor-metric strong{display:block;margin-top:3px;font:600 .7rem/1 var(--font-mono)}.pg-live-monitor-grid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(220px,1fr);gap:7px}.pg-live-monitor-panel{min-width:0;padding:7px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.pg-live-monitor-panel h3{margin:0 0 5px;padding-bottom:4px;border-bottom:1px solid var(--rule);font-size:.68rem}.pg-live-monitor-panel canvas{display:block;width:100%;height:130px;background:var(--paper);border:1px solid var(--rule)}.pg-live-monitor-params{margin:0;max-height:118px;overflow:hidden;font:500 .55rem/1.25 var(--font-mono);white-space:pre-wrap}.pg-live-monitor-actions{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}.pg-live-monitor-actions button{min-height:27px;padding:0 8px;font-size:.6rem}.pg-live-monitor-actions .pg-monitor-primary{border-color:var(--accent);background:var(--accent);color:var(--paper-2)}
|
| 144 |
.playground-metrics{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:5px;margin:10px 0}.playground-metric{padding:8px 9px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.playground-metric span{display:block;color:var(--ink-4);font:700 .5rem/1.2 var(--font-mono);letter-spacing:.07em;text-transform:uppercase}.playground-metric strong{display:block;margin-top:4px;color:var(--ink);font:600 .82rem/1.1 var(--font-mono)}
|
| 145 |
-
.pg-live-monitor-visuals{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px;margin-top:7px}.pg-live-monitor-tile{display:block;width:100%;padding:5px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2);color:var(--ink);text-align:left;cursor:pointer}.pg-live-monitor-tile:hover,.pg-live-monitor-tile:focus-visible{border-color:var(--accent);background:var(--accent-wash)}.pg-live-monitor-tile span{display:block;margin-bottom:3px;color:var(--ink-3);font:700 .47rem/1.1 var(--font-mono);text-transform:uppercase}.pg-live-monitor-tile canvas{width:100%;height:65px;background:var(--paper);border:1px solid var(--rule)}.pg-live-zoom{width:min(
|
| 146 |
.playground-viz-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.playground-viz{min-height:260px}.playground-viz canvas{width:100%;height:210px;display:block}
|
| 147 |
.playground-analysis-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px}.playground-analysis-card pre{font-size:.62rem;max-height:260px;overflow:auto;white-space:pre-wrap}
|
| 148 |
.playground-session-list{display:grid;gap:6px}.playground-session{display:flex;justify-content:space-between;gap:1rem;align-items:center;padding:9px 10px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.playground-session small{display:block;color:var(--ink-4)}
|
|
@@ -156,10 +158,26 @@ function injectStyles() {
|
|
| 156 |
function optionMarkup(items, labelKey = "label") {
|
| 157 |
return (items || []).filter((item) => item.available !== false).map((item) => {
|
| 158 |
const label = item[labelKey] || item.name;
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
}).join("");
|
| 161 |
}
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
function ensurePermanentBoundary(root) {
|
| 164 |
if (byId("pg-permanent-boundary")) return;
|
| 165 |
const boundary = document.createElement("div");
|
|
@@ -177,7 +195,7 @@ function buildPanels(root) {
|
|
| 177 |
<section data-generated-panel="builder" id="playground-builder">
|
| 178 |
<div class="pg-neutral-note"><strong>Neutraler Baukasten:</strong> Keine Modell- oder Topologieauswahl ist eine Empfehlung. <code>MHRN 5D</code> ist der Architektur-Default, weil er den nativen MHRN-Vertrag <code>(x,y,z,d4,d5)</code> verwendet — nicht weil 5D als überlegen gilt.</div>
|
| 179 |
<section class="playground-preset-deck" aria-labelledby="pg-preset-deck-title">
|
| 180 |
-
<div class="playground-preset-copy"><div><h3 id="pg-preset-deck-title">Preset Lab</h3><p>Empfohlene Startpunkte für Izhikevich und PAN. Presets verändern nur den Playground
|
| 181 |
<div class="playground-preset-controls"><label>Profil<select id="pg-user-preset-select"></select></label><label>Eigenes Preset speichern<input id="pg-user-preset-name" type="text" placeholder="z. B. PAN · Seed 2"></label><button type="button" id="pg-user-preset-apply">Anwenden</button><button type="button" id="pg-user-preset-save">Speichern</button><button type="button" id="pg-user-preset-delete">Eigenes löschen</button><p id="pg-user-preset-description" class="playground-preset-description">Preset auswählen oder eigene Einstellungen speichern.</p></div>
|
| 182 |
</section>
|
| 183 |
<div class="playground-grid">
|
|
@@ -214,7 +232,7 @@ function buildPanels(root) {
|
|
| 214 |
<label>Seed-Ensemble<input id="pg-ensemble" type="number" min="1" max="8" value="1"></label>
|
| 215 |
<label><span><input id="pg-persist" type="checkbox"> Session lokal speichern</span></label>
|
| 216 |
</article>
|
| 217 |
-
<article class="playground-card"><h3>05 · PAN-Hyperstate</h3>
|
| 218 |
<label><span><input id="pg-pan-enabled" type="checkbox"> PAN explorativ aktivieren</span></label>
|
| 219 |
<label>PAN-Dimensionen<input id="pg-pan-dimensions" type="number" min="5" max="32" value="5"></label>
|
| 220 |
<label><span><input id="pg-pan-closed-loop" type="checkbox" checked> Closed Loop</span></label>
|
|
@@ -243,6 +261,7 @@ function buildPanels(root) {
|
|
| 243 |
<label>Clock<select id="pg-clock-mode"><option value="continuous">Continuous</option><option value="dual">Dual · Event + Continuous</option></select></label>
|
| 244 |
<label>Base Hz<input id="pg-clock-base-hz" type="number" min="1" max="10000" value="100"></label>
|
| 245 |
<label>Event Batch ms<input id="pg-clock-event-batch" type="number" min="0.05" max="1000" step="0.05" value="10"></label>
|
|
|
|
| 246 |
<label>Execution<select id="pg-execution-mode"><option value="HYBRID_AUTO">HYBRID_AUTO</option><option value="EVENT_ONLY">EVENT_ONLY</option><option value="TICK_ONLY">TICK_ONLY</option></select></label>
|
| 247 |
<label>Initial Engine<select id="pg-execution-initial"><option value="EVENT_ONLY">EVENT_ONLY</option><option value="TICK_ONLY">TICK_ONLY</option></select></label>
|
| 248 |
<label>θ high<input id="pg-execution-high" type="number" min="0" max="1" step="0.01" value="0.30"></label>
|
|
@@ -276,7 +295,7 @@ function buildPanels(root) {
|
|
| 276 |
<label><span><input id="pg-cortical-plasticity" type="checkbox" checked> Layer-Gain plastisch</span></label>
|
| 277 |
<label>Layer Lernrate<input id="pg-cortical-lr" type="number" min="0" max="1" step="0.005" value="0.01"></label>
|
| 278 |
<label><span><input id="pg-behavior-enabled" type="checkbox"> Verhalten lernen</span></label>
|
| 279 |
-
<label>Aktionen<input id="pg-behavior-actions" type="number" min="
|
| 280 |
<label>Zielaktion<input id="pg-behavior-target" type="number" min="0" max="15" value="0"></label>
|
| 281 |
<label>Zielmodus<select id="pg-behavior-target-mode"><option value="cycle">cycle</option><option value="fixed">fixed</option></select></label>
|
| 282 |
<label>Min. Aktivität<input id="pg-behavior-min-activity" type="number" min="0" max="1" step="0.01" value="0.01"></label>
|
|
@@ -318,7 +337,7 @@ function buildPanels(root) {
|
|
| 318 |
<label><span><input id="pg-action-loop-enabled" type="checkbox"> Aktions-Loop aktiv</span></label>
|
| 319 |
<label>Loop Delay<input id="pg-action-loop-delay" type="number" min="1" max="64" value="1"></label>
|
| 320 |
<label>Persistenz<input id="pg-action-persistence" type="number" min="1" max="128" value="1"></label>
|
| 321 |
-
<label>Aktionsraum<input id="pg-action-space-size" type="number" min="
|
| 322 |
<label>Action → Input Map<input id="pg-action-to-input-map" value="auto"></label>
|
| 323 |
<label>Kopplungsstärke<input id="pg-action-coupling" type="number" min="0" max="10" step="0.1" value="0"></label>
|
| 324 |
<label>Aktionsrauschen<input id="pg-action-noise" type="number" min="0" max="1" step="0.01" value="0"></label>
|
|
@@ -328,6 +347,7 @@ function buildPanels(root) {
|
|
| 328 |
<label>Sandbox Sensor Noise<input id="pg-sandbox-sensor-noise" type="number" min="0" max="1" step="0.01" value="0.05"></label>
|
| 329 |
</article>
|
| 330 |
<article class="playground-card"><h3>12 · Ziel-Kodierung</h3>
|
|
|
|
| 331 |
<label>Kodierung<select id="pg-target-encoding"><option value="none">none</option><option value="one_hot">one_hot</option><option value="rate">rate</option><option value="population_latency">population_latency</option></select></label>
|
| 332 |
<label>Ziel sichtbar (Ticks)<input id="pg-target-persistence" type="number" min="1" max="256" value="1"></label>
|
| 333 |
<label>Ziel-Cue Strom<input id="pg-target-cue-current" type="number" min="0" max="500" step="0.5" value="0"></label>
|
|
@@ -366,20 +386,41 @@ function buildPanels(root) {
|
|
| 366 |
</article>
|
| 367 |
</div>
|
| 368 |
<div class="playground-actions"><button type="button" class="primary" id="pg-run">▶ Playground starten</button><button type="button" id="pg-robustness">Robustheitskontrollen</button><button type="button" id="pg-reset">Standardwerte</button></div>
|
| 369 |
-
<article class="playground-card playground-live-launcher"><h3>17 · PAN Live Monitor</h3><p>Die laufende PAN-Session mit Start/Pause, Input, Sandbox-Männchen und allen Live-Grafiken im Monitor-Popup.</p><div class="playground-actions"><button type="button" class="primary" id="pg-live-open">Live Monitor öffnen</button><button type="button" id="pg-live-clear">Temporäre Sessions löschen</button></div><pre id="pg-live-state">Keine Live-Session geöffnet.</pre></article>
|
| 370 |
-
<article class="playground-card"><h3>18 · CUDA & Parität</h3>
|
| 371 |
<div class="playground-grid">
|
| 372 |
<label>Target SM<input id="pg-cuda-target-sm" value="sm_86"></label>
|
| 373 |
<label>PTX Version<input id="pg-cuda-ptx-version" value="7.1"></label>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
<label><span><input id="pg-freeze-actions" type="checkbox"> Actions einfrieren</span></label>
|
| 375 |
<label><span><input id="pg-freeze-rewards" type="checkbox"> Rewards einfrieren</span></label>
|
| 376 |
<label>Referenz-Commit<input id="pg-parity-reference-commit" placeholder="Git SHA"></label>
|
| 377 |
</div>
|
| 378 |
<div class="playground-actions">
|
| 379 |
-
<button type="button" id="pg-
|
| 380 |
-
<button type="button" id="pg-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
</div>
|
| 382 |
-
<small>D1 = exakte Spike-Ereignisse · D2 = numerische Zustandsparität · D3 = Verhaltens-/Metrikparität. CUDA-Codegen ist vorhanden, ein echter GPU-Kernel-Launch ist hier noch nicht nachgewiesen.</small>
|
| 383 |
<pre id="pg-cuda-compiler-state">Noch kein CUDA-/Parity-Lauf.</pre>
|
| 384 |
</article>
|
| 385 |
<article class="playground-card"><h3>19 · Meta-Nachtlauf</h3>
|
|
@@ -431,6 +472,7 @@ function buildPanels(root) {
|
|
| 431 |
<section data-generated-panel="run" id="playground-run">
|
| 432 |
<div class="playground-metrics" id="pg-metrics"></div>
|
| 433 |
<div class="playground-viz-grid">
|
|
|
|
| 434 |
<article class="playground-viz"><h3>Spike Raster</h3><canvas id="pg-raster" width="800" height="300"></canvas></article>
|
| 435 |
<article class="playground-viz"><h3>Population Rate</h3><canvas id="pg-rate-canvas" width="800" height="300"></canvas></article>
|
| 436 |
<article class="playground-viz"><h3>Topologie · neutrale 2D-Projektion</h3><canvas id="pg-topology-canvas" width="800" height="300"></canvas></article>
|
|
@@ -522,6 +564,7 @@ function formPayload() {
|
|
| 522 |
clock_mode: byId("pg-clock-mode").value,
|
| 523 |
clock_base_hz: Number(byId("pg-clock-base-hz").value),
|
| 524 |
clock_event_batch_ms: Number(byId("pg-clock-event-batch").value),
|
|
|
|
| 525 |
execution_mode: byId("pg-execution-mode").value,
|
| 526 |
execution_initial_mode: byId("pg-execution-initial").value,
|
| 527 |
execution_theta_high: Number(byId("pg-execution-high").value),
|
|
@@ -644,6 +687,7 @@ function formPayload() {
|
|
| 644 |
frozen_reward_sequence: lastResult?.closed_loop?.reward_history||[],
|
| 645 |
parity_reference_source: "CPU_PYTHON_PLAYGROUND",
|
| 646 |
parity_reference_commit: byId("pg-parity-reference-commit")?.value?.trim()||"",
|
|
|
|
| 647 |
target_encoding: byId("pg-target-encoding").value,
|
| 648 |
target_persistence: Number(byId("pg-target-persistence").value),
|
| 649 |
target_cue_current: Number(byId("pg-target-cue-current").value),
|
|
@@ -720,14 +764,21 @@ function renderResult(result){
|
|
| 720 |
byId("pg-analysis-spike").textContent=JSON.stringify(a.spike_time||{},null,2);
|
| 721 |
byId("pg-analysis-network").textContent=JSON.stringify({network:net,dimensionality:dim,ensemble:result.ensemble||null,pan:result.pan||null,closed_loop:result.closed_loop||null,heterogeneity:result.heterogeneity||null,temporal_dynamics:result.temporal_dynamics||null,geometry:result.geometry||null,gates:result.gates||null,clock:result.clock||null,execution:result.execution||null,growth:result.growth||null,storage:result.storage||null},null,2);
|
| 722 |
byId("pg-analysis-plasticity").textContent=JSON.stringify({plasticity:a.plasticity||{},performance:a.performance||{},pan:result.pan||null,growth:result.growth||null,behavioral_learning:result.behavioral_learning||null,credit_assignment:result.credit_assignment||null,closed_loop:result.closed_loop||null,thalamic_gating:result.thalamic_gating||null,cortical_organization:result.cortical_organization||null,storage:result.storage||null,hardware:result.hardware||null},null,2);
|
| 723 |
-
byId("pg-analysis-io").textContent=JSON.stringify({neural_io:result.neural_io||{status:"disabled"},interfaces:result.interfaces||null},null,2);
|
| 724 |
byId("pg-run-json").textContent=JSON.stringify({session_id:result.session_id,manifest:result.manifest,model:result.model,config:result.config,metrics:result.metrics,readout:result.readout},null,2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 725 |
drawRaster(result);drawSeries("pg-rate-canvas",(result.monitors?.tick_spike_counts||[]).map(Number));drawTopology(result);drawState(result);drawSpectrum(result);drawDegree(result);
|
| 726 |
}
|
| 727 |
|
| 728 |
|
| 729 |
-
function drawLiveSandbox(world){
|
| 730 |
-
const item=ctxFor(
|
| 731 |
const{canvas,ctx}=item;
|
| 732 |
const px=p=>[canvas.width/2+Number(p.x||0)*170,canvas.height-30-Number(p.y||0)*150];
|
| 733 |
const links=[["head","neck"],["neck","hip"],["neck","shoulder_l"],["neck","shoulder_r"],["hip","knee_l"],["hip","knee_r"],["knee_l","foot_l"],["knee_r","foot_r"]];
|
|
@@ -738,12 +789,36 @@ function drawLiveSandbox(world){
|
|
| 738 |
let liveMonitorHistory=[];
|
| 739 |
let liveMonitorPaused=false;
|
| 740 |
let liveMonitorWorld=null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
|
| 742 |
function ensureLiveMonitor(){
|
| 743 |
let dialog=byId("pg-live-monitor");if(dialog)return dialog;
|
| 744 |
dialog=document.createElement("dialog");dialog.id="pg-live-monitor";dialog.className="pg-live-monitor";
|
| 745 |
dialog.innerHTML=`<header><div><span class="workspace-kicker">PAN LIVE MONITOR</span><h2>Live-Session</h2><p id="pg-live-monitor-status">Bereit</p></div><button type="button" class="pg-live-monitor-close" aria-label="Monitor schließen">×</button></header><div class="pg-live-monitor-body"><div class="pg-live-monitor-metrics"><div class="pg-live-monitor-metric"><span>Tick</span><strong id="pg-live-monitor-tick">0</strong></div><div class="pg-live-monitor-metric"><span>Spikes</span><strong id="pg-live-monitor-spikes">0</strong></div><div class="pg-live-monitor-metric"><span>Aktion</span><strong id="pg-live-monitor-action">—</strong></div><div class="pg-live-monitor-metric"><span>Reward</span><strong id="pg-live-monitor-reward">0</strong></div><div class="pg-live-monitor-metric"><span>Engine</span><strong id="pg-live-monitor-engine">—</strong></div></div><div class="pg-live-monitor-visuals"><button class="pg-live-monitor-tile" data-pg-live-expand="reset"><span>Reset</span><canvas id="pg-live-tile-reset" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="figure"><span>PAN-Männchen</span><canvas id="pg-live-tile-figure" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="raster"><span>Spike Raster</span><canvas id="pg-live-tile-raster" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="rate"><span>Populationsrate</span><canvas id="pg-live-tile-rate" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="topology"><span>Topologie · live</span><canvas id="pg-live-tile-topology" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="input"><span>Input</span><canvas id="pg-live-tile-input" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="output"><span>Output</span><canvas id="pg-live-tile-output" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="membrane"><span>Membranpotential</span><canvas id="pg-live-tile-membrane" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="spectrum"><span>Spectrum</span><canvas id="pg-live-tile-spectrum" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="degree"><span>Degree</span><canvas id="pg-live-tile-degree" width="220" height="120"></canvas></button></div><div class="pg-live-monitor-grid"><section class="pg-live-monitor-panel"><h3>Aktivität / Spike-Verlauf</h3><canvas id="pg-live-monitor-chart" width="760" height="260"></canvas></section><section class="pg-live-monitor-panel"><h3>Parameter</h3><pre id="pg-live-monitor-params" class="pg-live-monitor-params">—</pre></section><section class="pg-live-monitor-panel"><h3>Session</h3><pre id="pg-live-monitor-state" class="pg-live-monitor-params">—</pre></section></div><div class="pg-live-monitor-actions"><button type="button" id="pg-live-monitor-start" class="pg-monitor-primary">Start</button><button type="button" id="pg-live-monitor-pause">Pause</button><button type="button" id="pg-live-monitor-step">+32 Ticks</button><button type="button" id="pg-live-monitor-input">Input</button><button type="button" id="pg-live-monitor-stop">Stop</button><button type="button" id="pg-live-monitor-reset">Reset Ansicht</button></div></div>`;
|
| 746 |
-
document.body.append(dialog);
|
|
|
|
| 747 |
dialog.querySelector(".pg-live-monitor-close").addEventListener("click",()=>dialog.close());
|
| 748 |
dialog.querySelector("#pg-live-monitor-start").addEventListener("click",()=>{liveMonitorPaused=false;startLiveLoop().catch(error=>updateLiveMonitor({error:String(error.message||error)}));});
|
| 749 |
dialog.querySelector("#pg-live-monitor-pause").addEventListener("click",()=>{liveMonitorPaused=true;stopLiveLoop();updateLiveMonitor({status:"paused"});});
|
|
@@ -765,10 +840,16 @@ function drawLiveMonitorChart(){
|
|
| 765 |
const LIVE_TILE_TITLES={reset:"Live-Ansicht zurückgesetzt",figure:"PAN-Männchen",raster:"Spike Raster",rate:"Populationsrate",topology:"Topologie",input:"Input-Kanäle",output:"Output-Aktionen",membrane:"Membranpotential",spectrum:"Spectrum",degree:"Degree-Verteilung"};
|
| 766 |
function ensureLiveZoom(){
|
| 767 |
if(byId("pg-live-zoom"))return;
|
| 768 |
-
const dialog=document.createElement("dialog");dialog.id="pg-live-zoom";dialog.className="pg-live-zoom";dialog.innerHTML=`<header><strong id="pg-live-zoom-title">Live-Grafik</strong><button type="button" id="pg-live-zoom-close" aria-label="Grafik schließen">×</button></header><canvas id="pg-live-zoom-canvas" width="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 769 |
}
|
| 770 |
function openLiveZoom(kind){
|
| 771 |
-
ensureLiveZoom();const dialog=byId("pg-live-zoom"),source=byId(`pg-live-tile-${kind}`)
|
| 772 |
}
|
| 773 |
function drawTileSeries(id,values,color){
|
| 774 |
const canvas=byId(id),ctx=canvas?.getContext("2d");if(!canvas||!ctx)return;ctx.clearRect(0,0,canvas.width,canvas.height);ctx.strokeStyle=color||getComputedStyle(document.body).getPropertyValue("--accent")||"#b23a1a";ctx.lineWidth=2;ctx.beginPath();const data=values.length?values:[0];const max=Math.max(1,...data);data.forEach((value,index)=>{const x=5+index/Math.max(1,data.length-1)*(canvas.width-10);const y=canvas.height-5-(Number(value)||0)/max*(canvas.height-10);if(index===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);});ctx.stroke();
|
|
@@ -779,8 +860,8 @@ function drawLiveMonitorVisuals(payload={}){
|
|
| 779 |
const raster=byId("pg-live-tile-raster"),rasterCtx=raster?.getContext("2d");if(raster&&rasterCtx){rasterCtx.clearRect(0,0,raster.width,raster.height);rasterCtx.fillStyle=getComputedStyle(document.body).getPropertyValue("--accent")||"#b23a1a";spikes.slice(-160).forEach(item=>{rasterCtx.fillRect((Number(item.tick)||0)%100/100*raster.width,(Number(item.neuron_id)||0)%128/128*raster.height,2,2);});}
|
| 780 |
const membrane=byId("pg-live-tile-membrane"),mctx=membrane?.getContext("2d");if(membrane&&mctx){mctx.clearRect(0,0,membrane.width,membrane.height);mctx.strokeStyle=getComputedStyle(document.body).getPropertyValue("--moss")||"#2f5d47";mctx.lineWidth=2;const mean=Number(payload.mean_v??-65),min=Number(payload.min_v??-70),max=Number(payload.max_v??30);const y=membrane.height-5-(mean-min)/Math.max(1,max-min)*(membrane.height-10);mctx.beginPath();mctx.moveTo(5,y);mctx.lineTo(membrane.width-5,y);mctx.stroke();}
|
| 781 |
const inputValues=[Number(payload.input_active_neurons||0),Number(payload.input_peak||0)];drawTileSeries("pg-live-tile-input",inputValues,getComputedStyle(document.body).getPropertyValue("--accent")||"#b23a1a");drawTileSeries("pg-live-tile-output",counts.length?counts:[0],getComputedStyle(document.body).getPropertyValue("--moss")||"#2f5d47");
|
| 782 |
-
const topology=byId("pg-live-tile-topology"),tctx=topology?.getContext("2d"),topologyData=payload.topology; if(topology&&tctx){tctx.clearRect(0,0,topology.width,topology.height);const coords=topologyData?.coordinates||[],edges=topologyData?.edges||[];if(coords.length){const xs=coords.map(point=>Number(point[0]||0)),ys=coords.map(point=>Number(point[1]??point[2]??0)),minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys),scaleX=value=>(value-minX)/Math.max(1e-9,maxX-minX)*(topology.width-12)+6,scaleY=value=>(value-minY)/Math.max(1e-9,maxY-minY)*(topology.height-12)+6;tctx.strokeStyle=getComputedStyle(document.body).getPropertyValue("--rule-3")||"#777";tctx.globalAlpha=.25;edges.slice(0,600).forEach(edge=>{const source=coords[edge[0]],target=coords[edge[1]];if(!source||!target)return;tctx.beginPath();tctx.moveTo(scaleX(Number(source[0]||0)),scaleY(Number(source[1]??source[2]??0)));tctx.lineTo(scaleX(Number(target[0]||0)),scaleY(Number(target[1]??target[2]??0)));tctx.stroke();});tctx.globalAlpha=.9;tctx.fillStyle=getComputedStyle(document.body).getPropertyValue("--indigo")||"#2f4a7a";coords.forEach(point=>tctx.fillRect(scaleX(Number(point[0]||0))-1,scaleY(Number(point[1]??point[2]??0))-1,3,3));}}
|
| 783 |
-
if(payload.world)liveMonitorWorld=payload.world;drawLiveMonitorFigure(liveMonitorWorld,"pg-live-tile-figure");
|
| 784 |
}
|
| 785 |
|
| 786 |
function drawLiveMonitorFigure(world,targetId="pg-live-tile-figure"){
|
|
@@ -792,13 +873,13 @@ function updateLiveMonitor(payload={}){
|
|
| 792 |
const state=payload.state||payload;const tick=Number(state.tick??payload.tick??0);const spikes=Number(state.total_spikes??payload.total_spikes??0);const actions=payload.actions||state.actions||[];const rewards=payload.rewards||state.rewards||[];
|
| 793 |
if(Number.isFinite(spikes)){liveMonitorHistory.push({tick,spikes});if(liveMonitorHistory.length>120)liveMonitorHistory.shift();}
|
| 794 |
const set=(id,value)=>{const node=byId(id);if(node)node.textContent=String(value);};set("pg-live-monitor-tick",tick);set("pg-live-monitor-spikes",spikes);set("pg-live-monitor-action",actions.length?actions[actions.length-1]:"—");set("pg-live-monitor-reward",rewards.length?Number(rewards[rewards.length-1]).toFixed(3):"0");set("pg-live-monitor-engine",state.execution?.current_engine||payload.execution?.current_engine||"—");set("pg-live-monitor-status",payload.error||((liveMonitorPaused||!liveLoopTimer)?"Pausiert":"Läuft"));
|
| 795 |
-
const params={session_id:liveSessionId,tick,posture_score:payload.posture_score??payload.world?.posture_score??null,reward:payload.reward??null,reward_events:payload.reward_events||[],terminal:payload.terminal||null,pan_bias_current:byId("pg-pan-bias-current")?.value,feedback_gain:byId("pg-pan-feedback-gain")?.value,learning_rate:byId("pg-behavior-lr")?.value,epsilon:byId("pg-behavior-epsilon")?.value,edge_budget:byId("pg-edges")?.value};const paramsNode=byId("pg-live-monitor-params");if(paramsNode)paramsNode.textContent=JSON.stringify(params,null,2);const stateNode=byId("pg-live-monitor-state");if(stateNode)stateNode.textContent=JSON.stringify({state_digest:state.state_digest||payload.state_digest||null,input_queue_depth:state.input_queue_depth??payload.input_queue_depth??0,learning:state.learning||payload.learning||null},null,2);drawLiveMonitorChart();drawLiveMonitorVisuals(
|
| 796 |
}
|
| 797 |
|
| 798 |
async function createLiveSession(canRecover=true){
|
| 799 |
const payload={...formPayload(),neuron_model:"pan_adex_5d",pan_enabled:true,thalamic_relay_threshold:0,pan_bias_current:Number(byId("pg-pan-bias-current").value),behavior_target_mode:byId("pg-behavior-target-mode").value};
|
| 800 |
try{
|
| 801 |
-
const result=await apiPost("/api/playground/live/create",payload);liveSessionId=result.session_id;liveMonitorHistory=[];liveMonitorWorld=null;const monitor=ensureLiveMonitor();if(!monitor.open)monitor.showModal();byId("pg-live-state").textContent=JSON.stringify(result,null,2);updateLiveMonitor(result.state||result);
|
| 802 |
}catch(error){
|
| 803 |
if(canRecover&&/maximum live Playground sessions reached/i.test(String(error.message||error))){await clearLiveSessions();return createLiveSession(false);}
|
| 804 |
throw error;
|
|
@@ -817,16 +898,19 @@ async function clearLiveSessions(){
|
|
| 817 |
await Promise.all(sessions.map(session=>apiPost(`/api/playground/live/${encodeURIComponent(session.session_id)}/stop`,{}).catch(()=>null)));
|
| 818 |
result={classification:"PLAYGROUND_LIVE_SESSIONS_CLEARED",scientific_evidence:false,cleared:sessions.length,compatibility_cleanup:true};
|
| 819 |
}
|
| 820 |
-
liveSessionId=null;liveMonitorWorld=null;liveMonitorHistory=[];
|
| 821 |
byId("pg-live-state").textContent=JSON.stringify(result,null,2);
|
| 822 |
const monitor=byId("pg-live-monitor");if(monitor?.open)monitor.close();
|
| 823 |
}
|
| 824 |
async function stepLiveSession(){
|
| 825 |
if(!liveSessionId)throw new Error("Zuerst Live-Session starten.");
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
try{
|
| 829 |
-
|
|
|
|
|
|
|
|
|
|
| 830 |
}
|
| 831 |
async function startLiveLoop(){
|
| 832 |
if(!liveSessionId)throw new Error("Zuerst Live-Session starten.");
|
|
@@ -924,6 +1008,92 @@ async function checkCpuDeterminism(){
|
|
| 924 |
return result;
|
| 925 |
}
|
| 926 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 927 |
async function compileCudaGates(){
|
| 928 |
const node=byId("pg-cuda-compiler-state");
|
| 929 |
if(node)node.textContent="Gate-IR/PTX wird erzeugt …";
|
|
@@ -949,7 +1119,7 @@ async function runSession(){
|
|
| 949 |
|
| 950 |
async function runRobustness(){
|
| 951 |
const status=byId("pg-status");status.dataset.state="running";status.textContent="Explorative Robustheitskontrollen laufen …";
|
| 952 |
-
try{const result=await apiPost("/api/playground/robustness",{...formPayload(),persist:false,ensemble_runs:1});status.dataset.state="ok";status.textContent="Robustheitssuite abgeschlossen · nicht preregistriert · keine Evidenz";byId("pg-run-json").textContent=JSON.stringify(result,null,2);window.MHRNWorkspaceArchitecture?.selectRoute?.("playground","run");}
|
| 953 |
catch(error){status.dataset.state="error";status.textContent=`Robustheitssuite fehlgeschlagen: ${error.message||error}`;}
|
| 954 |
}
|
| 955 |
|
|
@@ -962,14 +1132,16 @@ function setBuilderValue(key,value){
|
|
| 962 |
pan_feedback_gain:"pan-feedback-gain",pan_feedback_nonlinearity:"pan-feedback-nonlinearity",pan_feedback_saturation:"pan-feedback-saturation",
|
| 963 |
action_loop_enabled:"action-loop-enabled",action_loop_delay:"action-loop-delay",action_persistence:"action-persistence",action_to_input_map:"action-to-input-map",action_space_size:"action-space-size",action_coupling_strength:"action-coupling",action_noise:"action-noise",
|
| 964 |
reward_signal_enabled:"reward-enabled",reward_magnitude:"reward-magnitude",reward_delay_ticks:"reward-delay",reward_shaping:"reward-shaping",reward_baseline:"reward-baseline",reward_decay:"reward-decay",reward_channel:"reward-channel",
|
| 965 |
-
target_encoding:"target-encoding",target_cue_channel:"target-cue-channel",target_cue_current:"target-cue-current",target_persistence:"target-persistence",target_shuffle:"target-shuffle",target_predictability:"target-predictability",
|
| 966 |
credit_assignment:"credit-assignment",credit_window:"credit-window",eligibility_trace_tau:"eligibility-tau",td_lambda:"td-lambda",gamma_discount:"gamma-discount",
|
| 967 |
neuron_threshold_variance:"threshold-variance",neuron_tau_m_variance:"tau-m-variance",inhibitory_fraction:"inhibitory-fraction",gaba_strength:"gaba-strength",e_i_ratio:"e-i-ratio",delay_distribution:"delay-distribution",delay_mean_ticks:"delay-mean",
|
| 968 |
refractory_variance:"refractory-variance",adaptation_strength:"adaptation-strength",adaptation_tau:"adaptation-tau",oscillation_enabled:"oscillation-enabled",oscillation_frequency:"oscillation-frequency",
|
| 969 |
input_topology:"input-topology",input_channels:"input-channels",input_noise_sigma:"input-noise",action_feedback_channel:"action-feedback-channel",reward_cue_channel:"reward-cue-channel",
|
| 970 |
geometry_input_coupling:"geometry-input-coupling",geometry_input_sigma:"geometry-input-sigma",sandbox_enabled:"sandbox-enabled",sandbox_sensor_noise:"sandbox-sensor-noise",
|
| 971 |
-
neuron_model:"neuron-model",synapse_model:"synapse-model",plasticity_rule:"plasticity",topology:"topology",readout:"readout",pan_enabled:"pan-enabled",behavior_learning_enabled:"behavior-enabled",stimulus:"stimulus"
|
|
|
|
| 972 |
};
|
|
|
|
| 973 |
const id="pg-"+(aliases[key]||key.replaceAll("_","-"));
|
| 974 |
const el=byId(id);if(!el)return;
|
| 975 |
if(el.type==="checkbox")el.checked=Boolean(value);
|
|
@@ -991,7 +1163,7 @@ function renderUserPresetOptions(){
|
|
| 991 |
const select=byId("pg-user-preset-select");if(!select)return;
|
| 992 |
const selected=select.value;
|
| 993 |
const presets=allPlaygroundPresets();
|
| 994 |
-
select.innerHTML=Object.entries(presets).map(([name,preset])=>`<option value="${name}">${preset.label||name}${BUILTIN_PLAYGROUND_PRESETS[name]?"":"
|
| 995 |
if(selected&&presets[selected])select.value=selected;else if(!select.value)select.value=presets[DEFAULT_PLAYGROUND_PRESET]?DEFAULT_PLAYGROUND_PRESET:"pan_exploration";
|
| 996 |
const preset=presets[select.value];
|
| 997 |
if(byId("pg-user-preset-description"))byId("pg-user-preset-description").textContent=preset?.description||"Preset auswählen oder eigene Einstellungen speichern.";
|
|
@@ -1058,13 +1230,21 @@ function ensureCatalogInfoDialog(){
|
|
| 1058 |
}
|
| 1059 |
|
| 1060 |
function showCatalogInfo(item){
|
| 1061 |
-
activeCatalogInfo=item;const language=catalogLanguage();const dialog=ensureCatalogInfoDialog();const
|
| 1062 |
}
|
| 1063 |
|
| 1064 |
function bindCatalogInfo(){
|
| 1065 |
const grid=byId("pg-catalog-grid");if(!grid||grid.dataset.infoBound)return;grid.dataset.infoBound="true";grid.addEventListener("click",event=>{const button=event.target.closest("[data-pg-catalog-info]");if(!button)return;const item=catalogInfoItems.get(button.dataset.pgCatalogInfo);if(item)showCatalogInfo(item);});
|
| 1066 |
}
|
| 1067 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1068 |
function renderCatalogCards(catalog){
|
| 1069 |
const panCandidates=(catalog.pan?.research_candidates||[]).map(item=>({name:item.id,note:item.question}));
|
| 1070 |
const panLiterature=(catalog.pan?.literature_context?.sources||[]).map(item=>({name:item.key,note:item.citation}));
|
|
@@ -1072,9 +1252,9 @@ function renderCatalogCards(catalog){
|
|
| 1072 |
const neuralIOCodecs=(catalog.neural_io?.codecs||[]).map(item=>({name:item.id,note:`${item.input_kind||""} · ${item.reconstruction_class||""}`}));
|
| 1073 |
const parityClasses=Object.entries(catalog.cuda_parity?.classes||{}).map(([name,item])=>({name,note:item.name||item.target_stage||""}));
|
| 1074 |
const groups=[["models",catalog.models],["topologies",catalog.topologies],["stimuli",catalog.stimuli],["synapses",catalog.synapses],["plasticity",catalog.plasticity],["readouts",catalog.readouts],["panCandidates",panCandidates],["panLiterature",panLiterature],["geometryLiterature",geometryLiterature],["neuralIO",neuralIOCodecs],["analyses",(catalog.analyses||[]).map(name=>({name}))],["robustness",(catalog.robustness_controls||[]).map(name=>({name}))],["cudaCompiler",(catalog.cuda_gate_compiler?.gate_types||[]).map(name=>({name,note:catalog.cuda_gate_compiler?.status||""}))],["cudaParity",parityClasses]];
|
| 1075 |
-
groups.push(["repo",PLAYGROUND_REPO_ELEMENTS.map(([name,category,description,source])=>({name,label:name,note:description,category,source}))]);
|
| 1076 |
catalogInfoItems=new Map();let itemIndex=0;const language=catalogLanguage();
|
| 1077 |
-
byId("pg-catalog-grid").innerHTML=groups.map(([categoryKey,items])=>{const title=catalogCategoryTitle(categoryKey,language);return `<article><h3>${playgroundEscape(title)}</h3>${(items||[]).map(item=>{const infoKey=String(itemIndex++);const info={...item,categoryKey};catalogInfoItems.set(infoKey,info);const description=describeCatalogItem(info,title,language);return `<button type="button" class="playground-chip" data-pg-catalog-info="${infoKey}" title="${playgroundEscape(description)}">${playgroundEscape(item.label||item.name)}</button>`;}).join("")}</article>`;}).join("");
|
| 1078 |
}
|
| 1079 |
|
| 1080 |
function renderCatalog(catalog){
|
|
@@ -1092,16 +1272,107 @@ function renderCatalog(catalog){
|
|
| 1092 |
const status=byId("pg-status");status.dataset.state="ok";status.textContent=`Bereit · bis ${catalog.limits.n_neurons} Neuronen · ${catalog.limits.edges} Kanten · ${catalog.limits.dimensions}D · scientific_evidence=false`;
|
| 1093 |
}
|
| 1094 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1095 |
export async function initPlayground(){
|
| 1096 |
const root=byId("tab-playground");if(!root)return;
|
| 1097 |
-
injectStyles();ensurePermanentBoundary(root);buildPanels(root);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1098 |
if(!root.dataset.languageBound){root.dataset.languageBound="true";document.addEventListener("mhrn:language-change",()=>{if(catalogState)renderCatalogCards(catalogState);const dialog=byId("pg-catalog-info-dialog");if(dialog?.open&&activeCatalogInfo)showCatalogInfo(activeCatalogInfo);});}
|
| 1099 |
-
byId("pg-run")?.addEventListener("click",runSession);byId("pg-cpu-determinism")?.addEventListener("click",()=>checkCpuDeterminism().catch(error=>{const node=byId("pg-cuda-compiler-state");if(node)node.textContent=String(error.message||error);}));byId("pg-cuda-compile")?.addEventListener("click",()=>compileCudaGates().catch(error=>{const node=byId("pg-cuda-compiler-state");if(node)node.textContent=String(error.message||error);}));byId("pg-user-preset-apply")?.addEventListener("click",applyUserPreset);byId("pg-user-preset-save")?.addEventListener("click",saveUserPreset);byId("pg-user-preset-delete")?.addEventListener("click",deleteUserPreset);byId("pg-user-preset-select")?.addEventListener("change",()=>{renderUserPresetOptions();applyUserPreset();});byId("pg-robustness")?.addEventListener("click",runRobustness);byId("pg-reset")?.addEventListener("click",resetForm);byId("pg-live-open")?.addEventListener("click",()=>openLiveMonitor().catch(error=>byId("pg-live-state").textContent=String(error.message||error)));byId("pg-live-clear")?.addEventListener("click",()=>clearLiveSessions().catch(error=>byId("pg-live-state").textContent=String(error.message||error)));byId("pg-night-start")?.addEventListener("click",()=>startNightRun().catch(error=>byId("pg-night-state").textContent=String(error.message||error)));byId("pg-night-stop")?.addEventListener("click",()=>stopNightRun().catch(error=>byId("pg-night-state").textContent=String(error.message||error)));byId("pg-night-refresh")?.addEventListener("click",()=>refreshNightStatus().catch(error=>byId("pg-night-state").textContent=String(error.message||error)));
|
| 1100 |
renderUserPresetOptions();
|
| 1101 |
try{renderCatalog(await apiGet("/api/playground/catalog"));renderUserPresetOptions();resetForm();}catch(error){const status=byId("pg-status");status.dataset.state="error";status.textContent=`Katalog nicht verfügbar: ${error.message}`;}
|
|
|
|
| 1102 |
byId("playground-builder")?.addEventListener("input", updateDefaultHints);
|
| 1103 |
byId("playground-builder")?.addEventListener("change", updateDefaultHints);
|
|
|
|
| 1104 |
await refreshSessions();
|
| 1105 |
try{await refreshNightStatus();}catch{ /* night manager is optional during partial deployments */ }
|
| 1106 |
-
|
|
|
|
| 1107 |
}
|
|
|
|
| 1 |
"use strict";
|
| 2 |
|
| 3 |
import { apiGet, apiPost, byId } from "../core/api.js";
|
| 4 |
+
import { initGuidedWorkspace } from "./playground-workspace.js";
|
| 5 |
import { getLanguage } from "../core/i18n.js?v=i18n-fix-20260920b";
|
| 6 |
|
| 7 |
let lastResult = null;
|
|
|
|
| 12 |
let catalogInfoItems = new Map();
|
| 13 |
let activeCatalogInfo = null;
|
| 14 |
const PLAYGROUND_PRESETS_STORAGE = "mhrn.playground.presets.v1";
|
| 15 |
+
const DEFAULT_PLAYGROUND_PRESET = "pan_full_balanced";
|
| 16 |
const BUILTIN_PLAYGROUND_PRESETS = {
|
| 17 |
izhikevich_reference: {
|
| 18 |
label: "Izhikevich · Referenz",
|
|
|
|
| 124 |
field.title = message;
|
| 125 |
field.closest("label")?.classList.toggle("pg-non-default", changed);
|
| 126 |
});
|
| 127 |
+
refreshCatalogOptionAvailability();
|
| 128 |
}
|
| 129 |
|
| 130 |
function injectStyles() {
|
|
|
|
| 144 |
.playground-status{padding:9px 11px;border:1px solid var(--rule);border-left:3px solid var(--rule-3);border-radius:var(--r-xs);background:var(--paper-2);color:var(--ink-2);font-size:.68rem;white-space:pre-wrap;overflow:auto}.playground-status[data-state="error"]{border-left-color:var(--crimson);color:var(--crimson)}.playground-status[data-state="ok"]{border-left-color:var(--moss);color:var(--moss)}
|
| 145 |
.pg-live-monitor{width:min(94vw,1000px);max-height:84vh;padding:0;border:1px solid var(--rule-3);border-radius:var(--r-md);background:var(--paper);color:var(--ink);box-shadow:var(--shadow-float)}.pg-live-monitor::backdrop{background:rgba(20,16,12,.55);backdrop-filter:blur(4px)}.pg-live-monitor>header{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:9px 12px;border-bottom:1px solid var(--rule);background:var(--paper-2)}.pg-live-monitor>header h2{margin:0;font-size:.95rem}.pg-live-monitor>header p{margin:2px 0 0;color:var(--ink-3);font-size:.61rem}.pg-live-monitor-close{min-width:28px;padding:0}.pg-live-monitor-body{padding:9px;overflow:hidden}.pg-live-monitor-metrics{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px;margin-bottom:7px}.pg-live-monitor-metric{padding:6px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.pg-live-monitor-metric span{display:block;color:var(--ink-4);font:700 .46rem/1.1 var(--font-mono);text-transform:uppercase}.pg-live-monitor-metric strong{display:block;margin-top:3px;font:600 .7rem/1 var(--font-mono)}.pg-live-monitor-grid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(220px,1fr);gap:7px}.pg-live-monitor-panel{min-width:0;padding:7px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.pg-live-monitor-panel h3{margin:0 0 5px;padding-bottom:4px;border-bottom:1px solid var(--rule);font-size:.68rem}.pg-live-monitor-panel canvas{display:block;width:100%;height:130px;background:var(--paper);border:1px solid var(--rule)}.pg-live-monitor-params{margin:0;max-height:118px;overflow:hidden;font:500 .55rem/1.25 var(--font-mono);white-space:pre-wrap}.pg-live-monitor-actions{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}.pg-live-monitor-actions button{min-height:27px;padding:0 8px;font-size:.6rem}.pg-live-monitor-actions .pg-monitor-primary{border-color:var(--accent);background:var(--accent);color:var(--paper-2)}
|
| 146 |
.playground-metrics{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:5px;margin:10px 0}.playground-metric{padding:8px 9px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.playground-metric span{display:block;color:var(--ink-4);font:700 .5rem/1.2 var(--font-mono);letter-spacing:.07em;text-transform:uppercase}.playground-metric strong{display:block;margin-top:4px;color:var(--ink);font:600 .82rem/1.1 var(--font-mono)}
|
| 147 |
+
.pg-live-monitor-visuals{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px;margin-top:7px}.pg-live-monitor-tile{display:block;width:100%;padding:5px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2);color:var(--ink);text-align:left;cursor:pointer}.pg-live-monitor-tile:hover,.pg-live-monitor-tile:focus-visible{border-color:var(--accent);background:var(--accent-wash)}.pg-live-monitor-tile span{display:block;margin-bottom:3px;color:var(--ink-3);font:700 .47rem/1.1 var(--font-mono);text-transform:uppercase}.pg-live-monitor-tile canvas{display:block;width:100%;height:65px;background:var(--paper);border:1px solid var(--rule);image-rendering:auto}.pg-live-zoom{width:min(96vw,1400px);max-height:92vh;padding:0;border:1px solid var(--rule-3);border-radius:var(--r-md);background:var(--paper);box-shadow:var(--shadow-float)}.pg-live-zoom::backdrop{background:rgba(20,16,12,.66);backdrop-filter:blur(5px)}.pg-live-zoom header{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:10px 13px;border-bottom:1px solid var(--rule);background:var(--paper-2)}.pg-live-zoom header strong{display:block;font-size:.96rem}.pg-live-zoom header p{margin:3px 0 0;color:var(--ink-3);font-size:.62rem}.pg-live-zoom-toolbar{display:flex;flex-wrap:wrap;gap:6px;padding:8px 13px;border-bottom:1px solid var(--rule);background:var(--paper-3)}.pg-live-zoom-toolbar button{min-height:30px;padding:0 9px;border:1px solid var(--rule-2);border-radius:var(--r-xs);background:var(--paper);color:var(--ink-2);font-size:.64rem}.pg-live-zoom-toolbar button:hover,.pg-live-zoom-toolbar button:focus-visible{border-color:var(--accent);background:var(--accent-wash)}.pg-live-zoom-stage{min-height:360px;padding:12px;overflow:auto;background:var(--paper)}.pg-live-zoom canvas{display:block;width:100%;height:min(72vh,760px);min-height:360px;background:var(--paper-2);border:1px solid var(--rule);image-rendering:auto}
|
| 148 |
.playground-viz-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.playground-viz{min-height:260px}.playground-viz canvas{width:100%;height:210px;display:block}
|
| 149 |
.playground-analysis-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px}.playground-analysis-card pre{font-size:.62rem;max-height:260px;overflow:auto;white-space:pre-wrap}
|
| 150 |
.playground-session-list{display:grid;gap:6px}.playground-session{display:flex;justify-content:space-between;gap:1rem;align-items:center;padding:9px 10px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.playground-session small{display:block;color:var(--ink-4)}
|
|
|
|
| 158 |
function optionMarkup(items, labelKey = "label") {
|
| 159 |
return (items || []).filter((item) => item.available !== false).map((item) => {
|
| 160 |
const label = item[labelKey] || item.name;
|
| 161 |
+
const executable = item.executable !== false;
|
| 162 |
+
const minimum = Number(item.min_dimensions || 0);
|
| 163 |
+
const status = executable ? "" : " · nur Katalog";
|
| 164 |
+
const note = item.note || (executable ? "Ausführbarer Playground-Referenzpfad." : "Nicht ausführbar; nur dokumentierter Katalogeintrag.");
|
| 165 |
+
return `<option value="${playgroundEscape(item.name)}" data-min-dimensions="${minimum}" data-executable="${String(executable)}" title="${playgroundEscape(note)}"${executable && !minimum ? "" : " disabled"}>${playgroundEscape(label)}${status}</option>`;
|
| 166 |
}).join("");
|
| 167 |
}
|
| 168 |
|
| 169 |
+
function refreshCatalogOptionAvailability() {
|
| 170 |
+
const dimensions = Number(byId("pg-dimensions")?.value || 5);
|
| 171 |
+
["pg-neuron-model", "pg-synapse-model", "pg-plasticity", "pg-readout", "pg-topology", "pg-stimulus"].forEach((id) => {
|
| 172 |
+
byId(id)?.querySelectorAll("option").forEach((option) => {
|
| 173 |
+
const executable = option.dataset.executable !== "false";
|
| 174 |
+
const minimum = Number(option.dataset.minDimensions || 0);
|
| 175 |
+
option.disabled = !executable || (minimum > 0 && dimensions < minimum);
|
| 176 |
+
if (minimum > 0 && dimensions < minimum) option.title = `Ausführbar ab ${minimum}D; bei ${dimensions}D nur Katalogeintrag.`;
|
| 177 |
+
});
|
| 178 |
+
});
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
function ensurePermanentBoundary(root) {
|
| 182 |
if (byId("pg-permanent-boundary")) return;
|
| 183 |
const boundary = document.createElement("div");
|
|
|
|
| 195 |
<section data-generated-panel="builder" id="playground-builder">
|
| 196 |
<div class="pg-neutral-note"><strong>Neutraler Baukasten:</strong> Keine Modell- oder Topologieauswahl ist eine Empfehlung. <code>MHRN 5D</code> ist der Architektur-Default, weil er den nativen MHRN-Vertrag <code>(x,y,z,d4,d5)</code> verwendet — nicht weil 5D als überlegen gilt.</div>
|
| 197 |
<section class="playground-preset-deck" aria-labelledby="pg-preset-deck-title">
|
| 198 |
+
<div class="playground-preset-copy"><div><h3 id="pg-preset-deck-title">Preset Lab</h3><p>Empfohlene Startpunkte für Izhikevich und PAN. Presets verändern nur den Playground. Eigene Profile liegen im Browser; der Katalog kommt vom Server.</p></div></div>
|
| 199 |
<div class="playground-preset-controls"><label>Profil<select id="pg-user-preset-select"></select></label><label>Eigenes Preset speichern<input id="pg-user-preset-name" type="text" placeholder="z. B. PAN · Seed 2"></label><button type="button" id="pg-user-preset-apply">Anwenden</button><button type="button" id="pg-user-preset-save">Speichern</button><button type="button" id="pg-user-preset-delete">Eigenes löschen</button><p id="pg-user-preset-description" class="playground-preset-description">Preset auswählen oder eigene Einstellungen speichern.</p></div>
|
| 200 |
</section>
|
| 201 |
<div class="playground-grid">
|
|
|
|
| 232 |
<label>Seed-Ensemble<input id="pg-ensemble" type="number" min="1" max="8" value="1"></label>
|
| 233 |
<label><span><input id="pg-persist" type="checkbox"> Session lokal speichern</span></label>
|
| 234 |
</article>
|
| 235 |
+
<article class="playground-card" id="pg-pan-settings"><h3>05 · PAN-Hyperstate</h3>
|
| 236 |
<label><span><input id="pg-pan-enabled" type="checkbox"> PAN explorativ aktivieren</span></label>
|
| 237 |
<label>PAN-Dimensionen<input id="pg-pan-dimensions" type="number" min="5" max="32" value="5"></label>
|
| 238 |
<label><span><input id="pg-pan-closed-loop" type="checkbox" checked> Closed Loop</span></label>
|
|
|
|
| 261 |
<label>Clock<select id="pg-clock-mode"><option value="continuous">Continuous</option><option value="dual">Dual · Event + Continuous</option></select></label>
|
| 262 |
<label>Base Hz<input id="pg-clock-base-hz" type="number" min="1" max="10000" value="100"></label>
|
| 263 |
<label>Event Batch ms<input id="pg-clock-event-batch" type="number" min="0.05" max="1000" step="0.05" value="10"></label>
|
| 264 |
+
<label>Neuron-/PAN-Ausfuehrung<select id="pg-neuron-backend"><option value="cpu">CPU-Referenz</option><option value="cuda_membrane">CUDA FP64 · Membran (PAN/Koerper auf CPU)</option><option value="cuda_pan">CUDA FP64 · PAN + synaptische Regeln (Koerper/Policy/Steuerung CPU)</option></select></label>
|
| 265 |
<label>Execution<select id="pg-execution-mode"><option value="HYBRID_AUTO">HYBRID_AUTO</option><option value="EVENT_ONLY">EVENT_ONLY</option><option value="TICK_ONLY">TICK_ONLY</option></select></label>
|
| 266 |
<label>Initial Engine<select id="pg-execution-initial"><option value="EVENT_ONLY">EVENT_ONLY</option><option value="TICK_ONLY">TICK_ONLY</option></select></label>
|
| 267 |
<label>θ high<input id="pg-execution-high" type="number" min="0" max="1" step="0.01" value="0.30"></label>
|
|
|
|
| 295 |
<label><span><input id="pg-cortical-plasticity" type="checkbox" checked> Layer-Gain plastisch</span></label>
|
| 296 |
<label>Layer Lernrate<input id="pg-cortical-lr" type="number" min="0" max="1" step="0.005" value="0.01"></label>
|
| 297 |
<label><span><input id="pg-behavior-enabled" type="checkbox"> Verhalten lernen</span></label>
|
| 298 |
+
<label>Aktionen<input id="pg-behavior-actions" type="number" min="1" max="16" value="4"></label>
|
| 299 |
<label>Zielaktion<input id="pg-behavior-target" type="number" min="0" max="15" value="0"></label>
|
| 300 |
<label>Zielmodus<select id="pg-behavior-target-mode"><option value="cycle">cycle</option><option value="fixed">fixed</option></select></label>
|
| 301 |
<label>Min. Aktivität<input id="pg-behavior-min-activity" type="number" min="0" max="1" step="0.01" value="0.01"></label>
|
|
|
|
| 337 |
<label><span><input id="pg-action-loop-enabled" type="checkbox"> Aktions-Loop aktiv</span></label>
|
| 338 |
<label>Loop Delay<input id="pg-action-loop-delay" type="number" min="1" max="64" value="1"></label>
|
| 339 |
<label>Persistenz<input id="pg-action-persistence" type="number" min="1" max="128" value="1"></label>
|
| 340 |
+
<label>Aktionsraum<input id="pg-action-space-size" type="number" min="1" max="32" value="4"></label>
|
| 341 |
<label>Action → Input Map<input id="pg-action-to-input-map" value="auto"></label>
|
| 342 |
<label>Kopplungsstärke<input id="pg-action-coupling" type="number" min="0" max="10" step="0.1" value="0"></label>
|
| 343 |
<label>Aktionsrauschen<input id="pg-action-noise" type="number" min="0" max="1" step="0.01" value="0"></label>
|
|
|
|
| 347 |
<label>Sandbox Sensor Noise<input id="pg-sandbox-sensor-noise" type="number" min="0" max="1" step="0.01" value="0.05"></label>
|
| 348 |
</article>
|
| 349 |
<article class="playground-card"><h3>12 · Ziel-Kodierung</h3>
|
| 350 |
+
<label>Cue-Kontrolle<select id="pg-target-cue-control"><option value="aligned">Zielreiz passend</option><option value="randomized">Zielreiz unabhaengig randomisiert</option><option value="absent">Zielreiz entfernt</option></select></label>
|
| 351 |
<label>Kodierung<select id="pg-target-encoding"><option value="none">none</option><option value="one_hot">one_hot</option><option value="rate">rate</option><option value="population_latency">population_latency</option></select></label>
|
| 352 |
<label>Ziel sichtbar (Ticks)<input id="pg-target-persistence" type="number" min="1" max="256" value="1"></label>
|
| 353 |
<label>Ziel-Cue Strom<input id="pg-target-cue-current" type="number" min="0" max="500" step="0.5" value="0"></label>
|
|
|
|
| 386 |
</article>
|
| 387 |
</div>
|
| 388 |
<div class="playground-actions"><button type="button" class="primary" id="pg-run">▶ Playground starten</button><button type="button" id="pg-robustness">Robustheitskontrollen</button><button type="button" id="pg-reset">Standardwerte</button></div>
|
| 389 |
+
<article class="playground-card playground-live-launcher" id="pg-pan-live-monitor"><h3>17 · PAN Live Monitor</h3><p>Die laufende PAN-Session mit Start/Pause, Input, Sandbox-Männchen und allen Live-Grafiken im Monitor-Popup.</p><div class="playground-actions"><button type="button" class="primary" id="pg-live-open">Live Monitor öffnen</button><button type="button" id="pg-live-clear">Temporäre Sessions löschen</button></div><pre id="pg-live-state">Keine Live-Session geöffnet.</pre></article>
|
| 390 |
+
<article class="playground-card" id="pg-cuda-controls"><h3>18 · CUDA & Parität</h3>
|
| 391 |
<div class="playground-grid">
|
| 392 |
<label>Target SM<input id="pg-cuda-target-sm" value="sm_86"></label>
|
| 393 |
<label>PTX Version<input id="pg-cuda-ptx-version" value="7.1"></label>
|
| 394 |
+
<label>GPU Neuronen<input id="pg-cuda-neurons" type="number" min="1" max="12288" value="256"></label>
|
| 395 |
+
<label>Blockgröße<select id="pg-cuda-block-size"><option>32</option><option selected>64</option><option>128</option><option>256</option><option>512</option></select></label>
|
| 396 |
+
<label>Device<input id="pg-cuda-device" type="number" min="0" value="0"></label>
|
| 397 |
+
<label>RNG Samples<input id="pg-cuda-rng-samples" type="number" min="1" max="4096" value="1000"></label>
|
| 398 |
<label><span><input id="pg-freeze-actions" type="checkbox"> Actions einfrieren</span></label>
|
| 399 |
<label><span><input id="pg-freeze-rewards" type="checkbox"> Rewards einfrieren</span></label>
|
| 400 |
<label>Referenz-Commit<input id="pg-parity-reference-commit" placeholder="Git SHA"></label>
|
| 401 |
</div>
|
| 402 |
<div class="playground-actions">
|
| 403 |
+
<button type="button" id="pg-cuda-status">CUDA-Status</button>
|
| 404 |
+
<button type="button" id="pg-cpu-determinism">CPU-Determinismus</button>
|
| 405 |
+
<button type="button" id="pg-cuda-compile">Gate IR / PTX</button>
|
| 406 |
+
<button type="button" id="pg-cuda-preflight">ptxas + Occupancy</button>
|
| 407 |
+
<button type="button" class="primary" id="pg-cuda-smoke">RTX Hardware-Smoke</button>
|
| 408 |
+
<button type="button" id="pg-cuda-rng">RNG-Parität</button>
|
| 409 |
+
<button type="button" id="pg-cuda-recurrent">CUDA-1.4 · 100 Ticks / 3 Blöcke</button>
|
| 410 |
+
<button type="button" id="pg-synaptic-transfer">Synaptischer Transfer · Kontrollvergleich</button>
|
| 411 |
+
<button type="button" id="pg-cue-controls">Cue-Decoding · 6 Kontrollen</button>
|
| 412 |
+
<button type="button" id="pg-cuda-builder-parity">D3 · Builder CPU/CUDA vergleichen</button>
|
| 413 |
+
<button type="button" id="pg-cuda-plasticity">CUDA-1.5 · STP/STDP-Parität</button>
|
| 414 |
+
</div>
|
| 415 |
+
<small>D1 = exakte Spike-Ereignisse · D2 = numerische Zustandsparität · D3 = Verhaltens-/Metrikparität. CUDA-1.3 prüft den 17-Parameter-Gate-ABI gegen dieselbe CPU-Gate-Referenz. GPU-Portierung von Membran-/Adaptations-/Refractory-State, Synapsen, Delays, Plastizität und Sandbox folgt in CUDA-1.4 bis 1.6.</small>
|
| 416 |
+
<div class="playground-analysis-grid">
|
| 417 |
+
<article class="playground-analysis-card"><h3>CUDA Reifegrad</h3><pre id="pg-cuda-stage-state">Status noch nicht geladen.</pre></article>
|
| 418 |
+
<article class="playground-analysis-card"><h3>ptxas & Occupancy</h3><pre id="pg-cuda-resource-state">Noch kein Preflight.</pre></article>
|
| 419 |
+
<article class="playground-analysis-card"><h3>D2 Hardware-Parität</h3><pre id="pg-cuda-parity-state">Noch kein Hardware-Smoke.</pre></article>
|
| 420 |
+
<article class="playground-analysis-card"><h3>RNG ε-greedy</h3><pre id="pg-cuda-rng-state">Noch kein RNG-Paritätstest.</pre></article>
|
| 421 |
+
<article class="playground-analysis-card"><h3>Rekurrente Membran-Parität</h3><p>129 AdEx-Neuronen, statische Synapsen und Delays; FP64-Referenz. Noch kein vollständiger PAN-GPU-Lauf.</p><pre id="pg-cuda-recurrent-state">Noch kein Mehrtakt-Test.</pre></article>
|
| 422 |
+
<article class="playground-analysis-card"><h3>GPU-Plastizität</h3><p>STP, STDP, Eligibility und eingefrorene Reward-Folge; FP64-Referenz. Der Live-Regelkreis ist ein separater Schritt.</p><pre id="pg-cuda-plasticity-state">Noch kein Plastizitätstest.</pre><p>Builder-D3: echte Aktionen und Koerpertrajektorie; Membran auf GPU, Synapsen und Umwelt auf CPU.</p><pre id="pg-cuda-builder-parity-state">Noch kein Builder-Vergleich.</pre><p>Cue-Experiment: passend / randomisiert / entfernt, jeweils mit und ohne Pair-STDP. Ohne Policy-Strom und Koerperrueckkopplung; kein automatischer Lernnachweis.</p><pre id="pg-cue-controls-state">Noch kein Cue-Experiment.</pre><p id="pg-transfer-summary">Transfer: vortrainierte Gewichte gegen frische Initialisierung auf neuen Cue-Kanaelen. Alle anderen Zustaende werden zurueckgesetzt.</p><details><summary>Transfer-Protokoll und Decoder-Kurven</summary><pre id="pg-transfer-state" style="max-height:360px;overflow:auto">Noch kein Transfer-Vergleich.</pre></details></article>
|
| 423 |
</div>
|
|
|
|
| 424 |
<pre id="pg-cuda-compiler-state">Noch kein CUDA-/Parity-Lauf.</pre>
|
| 425 |
</article>
|
| 426 |
<article class="playground-card"><h3>19 · Meta-Nachtlauf</h3>
|
|
|
|
| 472 |
<section data-generated-panel="run" id="playground-run">
|
| 473 |
<div class="playground-metrics" id="pg-metrics"></div>
|
| 474 |
<div class="playground-viz-grid">
|
| 475 |
+
<article class="playground-viz" id="pg-run-sandbox-panel" hidden><h3>PAN-Strichmann</h3><canvas id="pg-run-sandbox-canvas" width="800" height="360"></canvas><input id="pg-run-sandbox-frame" style="width:100%" type="range" min="0" max="0" value="0" aria-label="Strichmann-Zeitschritt"><p id="pg-run-sandbox-summary" aria-live="polite"></p><details><summary>Technische Frame-Daten</summary><pre id="pg-run-sandbox-state" style="max-height:240px;overflow:auto"></pre></details></article>
|
| 476 |
<article class="playground-viz"><h3>Spike Raster</h3><canvas id="pg-raster" width="800" height="300"></canvas></article>
|
| 477 |
<article class="playground-viz"><h3>Population Rate</h3><canvas id="pg-rate-canvas" width="800" height="300"></canvas></article>
|
| 478 |
<article class="playground-viz"><h3>Topologie · neutrale 2D-Projektion</h3><canvas id="pg-topology-canvas" width="800" height="300"></canvas></article>
|
|
|
|
| 564 |
clock_mode: byId("pg-clock-mode").value,
|
| 565 |
clock_base_hz: Number(byId("pg-clock-base-hz").value),
|
| 566 |
clock_event_batch_ms: Number(byId("pg-clock-event-batch").value),
|
| 567 |
+
neuron_backend: byId("pg-neuron-backend").value,
|
| 568 |
execution_mode: byId("pg-execution-mode").value,
|
| 569 |
execution_initial_mode: byId("pg-execution-initial").value,
|
| 570 |
execution_theta_high: Number(byId("pg-execution-high").value),
|
|
|
|
| 687 |
frozen_reward_sequence: lastResult?.closed_loop?.reward_history||[],
|
| 688 |
parity_reference_source: "CPU_PYTHON_PLAYGROUND",
|
| 689 |
parity_reference_commit: byId("pg-parity-reference-commit")?.value?.trim()||"",
|
| 690 |
+
target_cue_control: byId("pg-target-cue-control").value,
|
| 691 |
target_encoding: byId("pg-target-encoding").value,
|
| 692 |
target_persistence: Number(byId("pg-target-persistence").value),
|
| 693 |
target_cue_current: Number(byId("pg-target-cue-current").value),
|
|
|
|
| 764 |
byId("pg-analysis-spike").textContent=JSON.stringify(a.spike_time||{},null,2);
|
| 765 |
byId("pg-analysis-network").textContent=JSON.stringify({network:net,dimensionality:dim,ensemble:result.ensemble||null,pan:result.pan||null,closed_loop:result.closed_loop||null,heterogeneity:result.heterogeneity||null,temporal_dynamics:result.temporal_dynamics||null,geometry:result.geometry||null,gates:result.gates||null,clock:result.clock||null,execution:result.execution||null,growth:result.growth||null,storage:result.storage||null},null,2);
|
| 766 |
byId("pg-analysis-plasticity").textContent=JSON.stringify({plasticity:a.plasticity||{},performance:a.performance||{},pan:result.pan||null,growth:result.growth||null,behavioral_learning:result.behavioral_learning||null,credit_assignment:result.credit_assignment||null,closed_loop:result.closed_loop||null,thalamic_gating:result.thalamic_gating||null,cortical_organization:result.cortical_organization||null,storage:result.storage||null,hardware:result.hardware||null},null,2);
|
| 767 |
+
byId("pg-analysis-io").textContent=JSON.stringify({neural_io:result.neural_io||{status:"disabled"},cue_decoding:result.cue_decoding||null,interfaces:result.interfaces||null},null,2);
|
| 768 |
byId("pg-run-json").textContent=JSON.stringify({session_id:result.session_id,manifest:result.manifest,model:result.model,config:result.config,metrics:result.metrics,readout:result.readout},null,2);
|
| 769 |
+
const sandboxPanel=byId("pg-run-sandbox-panel"),sandboxSlider=byId("pg-run-sandbox-frame");
|
| 770 |
+
sandboxPanel.hidden=!result.sandbox?.world;
|
| 771 |
+
const frames=result.sandbox?.frames||[];
|
| 772 |
+
sandboxSlider.max=String(Math.max(0,frames.length-1));sandboxSlider.value=sandboxSlider.max;
|
| 773 |
+
const renderSandboxFrame=()=>{const frame=frames[Number(sandboxSlider.value)]||result.sandbox?.world;drawLiveSandbox(frame,"pg-run-sandbox-canvas");byId("pg-run-sandbox-summary").textContent=`Schritt ${Math.max(1,(result.sandbox?.ticks||frames.length)-frames.length+Number(sandboxSlider.value)+1)} / ${result.sandbox?.ticks||frames.length} · Haltung ${(Number(frame?.posture_score||0)*100).toFixed(1)} % · Aktion ${frame?.pan_action??"keine"} · Reward ${Number(frame?.reward||0).toFixed(3)}${frame?.terminal?` · ${frame.terminal}`:""}`;byId("pg-run-sandbox-state").textContent=JSON.stringify({backend:result.sandbox?.backend,ticks:result.sandbox?.ticks,dt_seconds:result.sandbox?.dt_seconds,posture_credit_updates:result.sandbox?.posture_credit_updates,frame},null,2);};
|
| 774 |
+
sandboxSlider.oninput=renderSandboxFrame;
|
| 775 |
+
if(!sandboxPanel.hidden)renderSandboxFrame();
|
| 776 |
drawRaster(result);drawSeries("pg-rate-canvas",(result.monitors?.tick_spike_counts||[]).map(Number));drawTopology(result);drawState(result);drawSpectrum(result);drawDegree(result);
|
| 777 |
}
|
| 778 |
|
| 779 |
|
| 780 |
+
function drawLiveSandbox(world,canvasId="pg-live-sandbox-canvas"){
|
| 781 |
+
const item=ctxFor(canvasId);if(!item||!world?.joints)return;
|
| 782 |
const{canvas,ctx}=item;
|
| 783 |
const px=p=>[canvas.width/2+Number(p.x||0)*170,canvas.height-30-Number(p.y||0)*150];
|
| 784 |
const links=[["head","neck"],["neck","hip"],["neck","shoulder_l"],["neck","shoulder_r"],["hip","knee_l"],["hip","knee_r"],["knee_l","foot_l"],["knee_r","foot_r"]];
|
|
|
|
| 789 |
let liveMonitorHistory=[];
|
| 790 |
let liveMonitorPaused=false;
|
| 791 |
let liveMonitorWorld=null;
|
| 792 |
+
let liveZoomKind=null;
|
| 793 |
+
let liveUpdateFactor=1;
|
| 794 |
+
let liveStepBusy=false;
|
| 795 |
+
let liveVisualFrame=0;
|
| 796 |
+
let pendingLivePayload=null;
|
| 797 |
+
let liveMonitorTopology=null;
|
| 798 |
+
let liveTopologyRefreshCounter=0;
|
| 799 |
+
|
| 800 |
+
function prepareLiveTileCanvases(root){
|
| 801 |
+
root.querySelectorAll(".pg-live-monitor-tile canvas").forEach(canvas=>{canvas.width=880;canvas.height=480;canvas.dataset.pgLiveHiDpi="true";});
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
function resizeLiveZoomCanvas(){
|
| 805 |
+
const canvas=byId("pg-live-zoom-canvas");if(!canvas)return;
|
| 806 |
+
const bounds=canvas.getBoundingClientRect();const ratio=Math.min(2,window.devicePixelRatio||1);const width=Math.max(640,Math.round(bounds.width*ratio));const height=Math.max(360,Math.round(bounds.height*ratio));
|
| 807 |
+
if(canvas.width!==width||canvas.height!==height){canvas.width=width;canvas.height=height;}
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
function refreshLiveZoom(){
|
| 811 |
+
if(!liveZoomKind)return;
|
| 812 |
+
const dialog=byId("pg-live-zoom"),source=byId(`pg-live-tile-${liveZoomKind}`),target=byId("pg-live-zoom-canvas");if(!dialog?.open||!source||!target)return;
|
| 813 |
+
resizeLiveZoomCanvas();const ctx=target.getContext("2d");ctx.imageSmoothingEnabled=false;ctx.clearRect(0,0,target.width,target.height);ctx.drawImage(source,0,0,target.width,target.height);
|
| 814 |
+
}
|
| 815 |
|
| 816 |
function ensureLiveMonitor(){
|
| 817 |
let dialog=byId("pg-live-monitor");if(dialog)return dialog;
|
| 818 |
dialog=document.createElement("dialog");dialog.id="pg-live-monitor";dialog.className="pg-live-monitor";
|
| 819 |
dialog.innerHTML=`<header><div><span class="workspace-kicker">PAN LIVE MONITOR</span><h2>Live-Session</h2><p id="pg-live-monitor-status">Bereit</p></div><button type="button" class="pg-live-monitor-close" aria-label="Monitor schließen">×</button></header><div class="pg-live-monitor-body"><div class="pg-live-monitor-metrics"><div class="pg-live-monitor-metric"><span>Tick</span><strong id="pg-live-monitor-tick">0</strong></div><div class="pg-live-monitor-metric"><span>Spikes</span><strong id="pg-live-monitor-spikes">0</strong></div><div class="pg-live-monitor-metric"><span>Aktion</span><strong id="pg-live-monitor-action">—</strong></div><div class="pg-live-monitor-metric"><span>Reward</span><strong id="pg-live-monitor-reward">0</strong></div><div class="pg-live-monitor-metric"><span>Engine</span><strong id="pg-live-monitor-engine">—</strong></div></div><div class="pg-live-monitor-visuals"><button class="pg-live-monitor-tile" data-pg-live-expand="reset"><span>Reset</span><canvas id="pg-live-tile-reset" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="figure"><span>PAN-Männchen</span><canvas id="pg-live-tile-figure" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="raster"><span>Spike Raster</span><canvas id="pg-live-tile-raster" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="rate"><span>Populationsrate</span><canvas id="pg-live-tile-rate" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="topology"><span>Topologie · live</span><canvas id="pg-live-tile-topology" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="input"><span>Input</span><canvas id="pg-live-tile-input" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="output"><span>Output</span><canvas id="pg-live-tile-output" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="membrane"><span>Membranpotential</span><canvas id="pg-live-tile-membrane" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="spectrum"><span>Spectrum</span><canvas id="pg-live-tile-spectrum" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="degree"><span>Degree</span><canvas id="pg-live-tile-degree" width="220" height="120"></canvas></button></div><div class="pg-live-monitor-grid"><section class="pg-live-monitor-panel"><h3>Aktivität / Spike-Verlauf</h3><canvas id="pg-live-monitor-chart" width="760" height="260"></canvas></section><section class="pg-live-monitor-panel"><h3>Parameter</h3><pre id="pg-live-monitor-params" class="pg-live-monitor-params">—</pre></section><section class="pg-live-monitor-panel"><h3>Session</h3><pre id="pg-live-monitor-state" class="pg-live-monitor-params">—</pre></section></div><div class="pg-live-monitor-actions"><button type="button" id="pg-live-monitor-start" class="pg-monitor-primary">Start</button><button type="button" id="pg-live-monitor-pause">Pause</button><button type="button" id="pg-live-monitor-step">+32 Ticks</button><button type="button" id="pg-live-monitor-input">Input</button><button type="button" id="pg-live-monitor-stop">Stop</button><button type="button" id="pg-live-monitor-reset">Reset Ansicht</button></div></div>`;
|
| 820 |
+
document.body.append(dialog);prepareLiveTileCanvases(dialog);
|
| 821 |
+
const actions=dialog.querySelector(".pg-live-monitor-actions");if(actions&&!byId("pg-live-update-factor")){const scale=document.createElement("label");scale.className="pg-live-update-scale";scale.innerHTML='<span>UI-Update</span><select id="pg-live-update-factor" aria-label="Simulation steps per UI update"><option value="1">1x</option><option value="5">5x</option><option value="10">10x</option><option value="25">25x</option><option value="50">50x</option><option value="100">100x</option></select>';actions.prepend(scale);scale.querySelector("select").addEventListener("change",event=>{liveUpdateFactor=Math.max(1,Number(event.target.value)||1);updateLiveZoomState();});}
|
| 822 |
dialog.querySelector(".pg-live-monitor-close").addEventListener("click",()=>dialog.close());
|
| 823 |
dialog.querySelector("#pg-live-monitor-start").addEventListener("click",()=>{liveMonitorPaused=false;startLiveLoop().catch(error=>updateLiveMonitor({error:String(error.message||error)}));});
|
| 824 |
dialog.querySelector("#pg-live-monitor-pause").addEventListener("click",()=>{liveMonitorPaused=true;stopLiveLoop();updateLiveMonitor({status:"paused"});});
|
|
|
|
| 840 |
const LIVE_TILE_TITLES={reset:"Live-Ansicht zurückgesetzt",figure:"PAN-Männchen",raster:"Spike Raster",rate:"Populationsrate",topology:"Topologie",input:"Input-Kanäle",output:"Output-Aktionen",membrane:"Membranpotential",spectrum:"Spectrum",degree:"Degree-Verteilung"};
|
| 841 |
function ensureLiveZoom(){
|
| 842 |
if(byId("pg-live-zoom"))return;
|
| 843 |
+
const dialog=document.createElement("dialog");dialog.id="pg-live-zoom";dialog.className="pg-live-zoom";dialog.innerHTML=`<header><div><span class="workspace-kicker">LIVE DETAIL VIEW</span><strong id="pg-live-zoom-title">Live-Grafik</strong><p id="pg-live-zoom-state">Bereit</p></div><button type="button" id="pg-live-zoom-close" aria-label="Grafik schließen">×</button></header><div class="pg-live-zoom-toolbar"><button type="button" id="pg-live-zoom-start">Start</button><button type="button" id="pg-live-zoom-pause">Pause</button><button type="button" id="pg-live-zoom-step">Schritt</button><button type="button" id="pg-live-zoom-input">Input senden</button><button type="button" id="pg-live-zoom-reset">Reset</button><button type="button" id="pg-live-zoom-stop">Stop</button></div><div class="pg-live-zoom-stage"><canvas id="pg-live-zoom-canvas" width="1600" height="900"></canvas></div>`;document.body.append(dialog);dialog.querySelector("#pg-live-zoom-close").addEventListener("click",()=>{liveZoomKind=null;dialog.close();});dialog.querySelector("#pg-live-zoom-start").addEventListener("click",()=>startLiveLoop().catch(error=>updateLiveZoomState(error.message)));dialog.querySelector("#pg-live-zoom-pause").addEventListener("click",()=>{stopLiveLoop();updateLiveZoomState("Pausiert");});dialog.querySelector("#pg-live-zoom-step").addEventListener("click",()=>stepLiveSession().catch(error=>updateLiveZoomState(error.message)));dialog.querySelector("#pg-live-zoom-input").addEventListener("click",()=>injectLiveInput().catch(error=>updateLiveZoomState(error.message)));dialog.querySelector("#pg-live-zoom-reset").addEventListener("click",()=>{resetForm();liveMonitorHistory=[];drawLiveMonitorVisuals({});updateLiveZoomState("Ansicht zurückgesetzt");});dialog.querySelector("#pg-live-zoom-stop").addEventListener("click",()=>stopLiveSession().catch(error=>updateLiveZoomState(error.message)));dialog.addEventListener("close",()=>{liveZoomKind=null;});window.addEventListener("resize",refreshLiveZoom);
|
| 844 |
+
}
|
| 845 |
+
function currentLiveUpdateFactor(){return Math.max(1,Number(byId("pg-live-update-factor")?.value)||liveUpdateFactor);}
|
| 846 |
+
function updateLiveZoomState(message){const node=byId("pg-live-zoom-state");if(node)node.textContent=`${String(message||((liveMonitorPaused||!liveLoopTimer)?"Pausiert":"Läuft"))} · ${currentLiveUpdateFactor()}x UI-Update`;}
|
| 847 |
+
function ensureLiveZoomScaleControl(){
|
| 848 |
+
const toolbar=byId("pg-live-zoom")?.querySelector(".pg-live-zoom-toolbar");if(!toolbar||byId("pg-live-zoom-factor"))return;
|
| 849 |
+
const label=document.createElement("label");label.textContent="UI-Update";const select=document.createElement("select");select.id="pg-live-zoom-factor";select.setAttribute("aria-label","Simulation steps per UI update");select.innerHTML='<option value="1">1x</option><option value="5">5x</option><option value="10">10x</option><option value="25">25x</option><option value="50">50x</option><option value="100">100x</option>';select.value=String(liveUpdateFactor);select.addEventListener("change",event=>{liveUpdateFactor=Math.max(1,Number(event.target.value)||1);const monitorSelect=byId("pg-live-update-factor");if(monitorSelect)monitorSelect.value=String(liveUpdateFactor);updateLiveZoomState();});label.prepend(select);toolbar.prepend(label);
|
| 850 |
}
|
| 851 |
function openLiveZoom(kind){
|
| 852 |
+
ensureLiveZoom();ensureLiveZoomScaleControl();const dialog=byId("pg-live-zoom"),source=byId(`pg-live-tile-${kind}`);if(!dialog||!source)return;liveZoomKind=kind;byId("pg-live-zoom-title").textContent=LIVE_TILE_TITLES[kind]||kind;dialog.showModal();resizeLiveZoomCanvas();refreshLiveZoom();updateLiveZoomState();
|
| 853 |
}
|
| 854 |
function drawTileSeries(id,values,color){
|
| 855 |
const canvas=byId(id),ctx=canvas?.getContext("2d");if(!canvas||!ctx)return;ctx.clearRect(0,0,canvas.width,canvas.height);ctx.strokeStyle=color||getComputedStyle(document.body).getPropertyValue("--accent")||"#b23a1a";ctx.lineWidth=2;ctx.beginPath();const data=values.length?values:[0];const max=Math.max(1,...data);data.forEach((value,index)=>{const x=5+index/Math.max(1,data.length-1)*(canvas.width-10);const y=canvas.height-5-(Number(value)||0)/max*(canvas.height-10);if(index===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);});ctx.stroke();
|
|
|
|
| 860 |
const raster=byId("pg-live-tile-raster"),rasterCtx=raster?.getContext("2d");if(raster&&rasterCtx){rasterCtx.clearRect(0,0,raster.width,raster.height);rasterCtx.fillStyle=getComputedStyle(document.body).getPropertyValue("--accent")||"#b23a1a";spikes.slice(-160).forEach(item=>{rasterCtx.fillRect((Number(item.tick)||0)%100/100*raster.width,(Number(item.neuron_id)||0)%128/128*raster.height,2,2);});}
|
| 861 |
const membrane=byId("pg-live-tile-membrane"),mctx=membrane?.getContext("2d");if(membrane&&mctx){mctx.clearRect(0,0,membrane.width,membrane.height);mctx.strokeStyle=getComputedStyle(document.body).getPropertyValue("--moss")||"#2f5d47";mctx.lineWidth=2;const mean=Number(payload.mean_v??-65),min=Number(payload.min_v??-70),max=Number(payload.max_v??30);const y=membrane.height-5-(mean-min)/Math.max(1,max-min)*(membrane.height-10);mctx.beginPath();mctx.moveTo(5,y);mctx.lineTo(membrane.width-5,y);mctx.stroke();}
|
| 862 |
const inputValues=[Number(payload.input_active_neurons||0),Number(payload.input_peak||0)];drawTileSeries("pg-live-tile-input",inputValues,getComputedStyle(document.body).getPropertyValue("--accent")||"#b23a1a");drawTileSeries("pg-live-tile-output",counts.length?counts:[0],getComputedStyle(document.body).getPropertyValue("--moss")||"#2f5d47");
|
| 863 |
+
if(payload.topology)liveMonitorTopology=payload.topology;const topology=byId("pg-live-tile-topology"),tctx=topology?.getContext("2d"),topologyData=payload.topology||liveMonitorTopology; if(topology&&tctx){tctx.clearRect(0,0,topology.width,topology.height);const coords=topologyData?.coordinates||[],edges=topologyData?.edges||[];if(coords.length){const xs=coords.map(point=>Number(point[0]||0)),ys=coords.map(point=>Number(point[1]??point[2]??0)),minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys),scaleX=value=>(value-minX)/Math.max(1e-9,maxX-minX)*(topology.width-12)+6,scaleY=value=>(value-minY)/Math.max(1e-9,maxY-minY)*(topology.height-12)+6;tctx.strokeStyle=getComputedStyle(document.body).getPropertyValue("--rule-3")||"#777";tctx.globalAlpha=.25;edges.slice(0,600).forEach(edge=>{const source=coords[edge[0]],target=coords[edge[1]];if(!source||!target)return;tctx.beginPath();tctx.moveTo(scaleX(Number(source[0]||0)),scaleY(Number(source[1]??source[2]??0)));tctx.lineTo(scaleX(Number(target[0]||0)),scaleY(Number(target[1]??target[2]??0)));tctx.stroke();});tctx.globalAlpha=.9;tctx.fillStyle=getComputedStyle(document.body).getPropertyValue("--indigo")||"#2f4a7a";coords.forEach(point=>tctx.fillRect(scaleX(Number(point[0]||0))-1,scaleY(Number(point[1]??point[2]??0))-1,3,3));}}
|
| 864 |
+
if(payload.world)liveMonitorWorld=payload.world;drawLiveMonitorFigure(liveMonitorWorld,"pg-live-tile-figure");refreshLiveZoom();
|
| 865 |
}
|
| 866 |
|
| 867 |
function drawLiveMonitorFigure(world,targetId="pg-live-tile-figure"){
|
|
|
|
| 873 |
const state=payload.state||payload;const tick=Number(state.tick??payload.tick??0);const spikes=Number(state.total_spikes??payload.total_spikes??0);const actions=payload.actions||state.actions||[];const rewards=payload.rewards||state.rewards||[];
|
| 874 |
if(Number.isFinite(spikes)){liveMonitorHistory.push({tick,spikes});if(liveMonitorHistory.length>120)liveMonitorHistory.shift();}
|
| 875 |
const set=(id,value)=>{const node=byId(id);if(node)node.textContent=String(value);};set("pg-live-monitor-tick",tick);set("pg-live-monitor-spikes",spikes);set("pg-live-monitor-action",actions.length?actions[actions.length-1]:"—");set("pg-live-monitor-reward",rewards.length?Number(rewards[rewards.length-1]).toFixed(3):"0");set("pg-live-monitor-engine",state.execution?.current_engine||payload.execution?.current_engine||"—");set("pg-live-monitor-status",payload.error||((liveMonitorPaused||!liveLoopTimer)?"Pausiert":"Läuft"));
|
| 876 |
+
const params={session_id:liveSessionId,tick,posture_score:payload.posture_score??payload.world?.posture_score??null,reward:payload.reward??null,reward_events:payload.reward_events||[],terminal:payload.terminal||null,pan_bias_current:byId("pg-pan-bias-current")?.value,feedback_gain:byId("pg-pan-feedback-gain")?.value,learning_rate:byId("pg-behavior-lr")?.value,epsilon:byId("pg-behavior-epsilon")?.value,edge_budget:byId("pg-edges")?.value};const paramsNode=byId("pg-live-monitor-params");if(paramsNode)paramsNode.textContent=JSON.stringify(params,null,2);const stateNode=byId("pg-live-monitor-state");if(stateNode)stateNode.textContent=JSON.stringify({state_digest:state.state_digest||payload.state_digest||null,input_queue_depth:state.input_queue_depth??payload.input_queue_depth??0,learning:state.learning||payload.learning||null},null,2);pendingLivePayload=payload;if(!liveVisualFrame){liveVisualFrame=requestAnimationFrame(()=>{liveVisualFrame=0;const nextPayload=pendingLivePayload||{};pendingLivePayload=null;drawLiveMonitorChart();drawLiveMonitorVisuals(nextPayload);drawLiveMonitorFigure(nextPayload.world||(nextPayload.state||{}).world||null);});}updateLiveZoomState(payload.error);
|
| 877 |
}
|
| 878 |
|
| 879 |
async function createLiveSession(canRecover=true){
|
| 880 |
const payload={...formPayload(),neuron_model:"pan_adex_5d",pan_enabled:true,thalamic_relay_threshold:0,pan_bias_current:Number(byId("pg-pan-bias-current").value),behavior_target_mode:byId("pg-behavior-target-mode").value};
|
| 881 |
try{
|
| 882 |
+
const result=await apiPost("/api/playground/live/create",payload);liveSessionId=result.session_id;liveMonitorHistory=[];liveMonitorWorld=null;liveMonitorTopology=null;liveTopologyRefreshCounter=0;const monitor=ensureLiveMonitor();if(!monitor.open)monitor.showModal();byId("pg-live-state").textContent=JSON.stringify(result,null,2);updateLiveMonitor(result.state||result);
|
| 883 |
}catch(error){
|
| 884 |
if(canRecover&&/maximum live Playground sessions reached/i.test(String(error.message||error))){await clearLiveSessions();return createLiveSession(false);}
|
| 885 |
throw error;
|
|
|
|
| 898 |
await Promise.all(sessions.map(session=>apiPost(`/api/playground/live/${encodeURIComponent(session.session_id)}/stop`,{}).catch(()=>null)));
|
| 899 |
result={classification:"PLAYGROUND_LIVE_SESSIONS_CLEARED",scientific_evidence:false,cleared:sessions.length,compatibility_cleanup:true};
|
| 900 |
}
|
| 901 |
+
liveSessionId=null;liveMonitorWorld=null;liveMonitorTopology=null;liveTopologyRefreshCounter=0;liveMonitorHistory=[];
|
| 902 |
byId("pg-live-state").textContent=JSON.stringify(result,null,2);
|
| 903 |
const monitor=byId("pg-live-monitor");if(monitor?.open)monitor.close();
|
| 904 |
}
|
| 905 |
async function stepLiveSession(){
|
| 906 |
if(!liveSessionId)throw new Error("Zuerst Live-Session starten.");
|
| 907 |
+
if(liveStepBusy)return null;
|
| 908 |
+
liveStepBusy=true;
|
| 909 |
+
try{
|
| 910 |
+
const factor=currentLiveUpdateFactor();const includeTopology=liveMonitorTopology===null||liveTopologyRefreshCounter++%10===0;const result=await apiPost(`/api/playground/live/${encodeURIComponent(liveSessionId)}/step`,{ticks:Math.min(4096,32*factor),include_topology:includeTopology});
|
| 911 |
+
try{const sandbox=await apiPost(`/api/playground/live/${encodeURIComponent(liveSessionId)}/sandbox`,{ticks:Math.min(64,Math.max(8,8*factor))});result.world=sandbox.world;liveMonitorWorld=sandbox.world;}catch{}
|
| 912 |
+
byId("pg-live-state").textContent=JSON.stringify(result,null,2);updateLiveMonitor(result);return result;
|
| 913 |
+
} finally {liveStepBusy=false;}
|
| 914 |
}
|
| 915 |
async function startLiveLoop(){
|
| 916 |
if(!liveSessionId)throw new Error("Zuerst Live-Session starten.");
|
|
|
|
| 1008 |
return result;
|
| 1009 |
}
|
| 1010 |
|
| 1011 |
+
function cudaRequestPayload(){
|
| 1012 |
+
return {
|
| 1013 |
+
...formPayload(),
|
| 1014 |
+
target_sm:byId("pg-cuda-target-sm")?.value||"sm_86",
|
| 1015 |
+
ptx_version:byId("pg-cuda-ptx-version")?.value||"7.1",
|
| 1016 |
+
n_neurons:Number(byId("pg-cuda-neurons")?.value||256),
|
| 1017 |
+
block_size:Number(byId("pg-cuda-block-size")?.value||64),
|
| 1018 |
+
device_ordinal:Number(byId("pg-cuda-device")?.value||0),
|
| 1019 |
+
reference_commit:byId("pg-parity-reference-commit")?.value||"",
|
| 1020 |
+
rng_samples:Number(byId("pg-cuda-rng-samples")?.value||1000),
|
| 1021 |
+
};
|
| 1022 |
+
}
|
| 1023 |
+
|
| 1024 |
+
async function refreshCudaStatus(){
|
| 1025 |
+
const node=byId("pg-cuda-stage-state");
|
| 1026 |
+
if(node)node.textContent="CUDA-Laufzeit wird geprüft …";
|
| 1027 |
+
const result=await apiGet("/api/playground/cuda/status");
|
| 1028 |
+
if(node)node.textContent=JSON.stringify({
|
| 1029 |
+
target_default:result.target_default,
|
| 1030 |
+
ptxas_available:result.ptxas_available,
|
| 1031 |
+
cuda_driver_available:result.cuda_driver_available,
|
| 1032 |
+
cuda_driver_error:result.cuda_driver_error,
|
| 1033 |
+
stages:result.stages,
|
| 1034 |
+
application_cpu:result.application_cpu,
|
| 1035 |
+
gpu_porting:result.gpu_porting,
|
| 1036 |
+
verification:result.verification,
|
| 1037 |
+
},null,2);
|
| 1038 |
+
return result;
|
| 1039 |
+
}
|
| 1040 |
+
|
| 1041 |
+
async function runCudaPreflight(){
|
| 1042 |
+
const node=byId("pg-cuda-resource-state");
|
| 1043 |
+
if(node)node.textContent="ptxas + Driver-Load + Occupancy werden geprüft …";
|
| 1044 |
+
const result=await apiPost("/api/playground/cuda/preflight",cudaRequestPayload());
|
| 1045 |
+
if(node)node.textContent=JSON.stringify({
|
| 1046 |
+
execution_status:result.execution_status,
|
| 1047 |
+
ready_for_cooperative_launch:result.ready_for_cooperative_launch,
|
| 1048 |
+
ptxas:result.ptxas,
|
| 1049 |
+
cooperative:result.cooperative,
|
| 1050 |
+
},null,2);
|
| 1051 |
+
return result;
|
| 1052 |
+
}
|
| 1053 |
+
|
| 1054 |
+
async function runCudaHardwareSmoke(){
|
| 1055 |
+
const node=byId("pg-cuda-parity-state");
|
| 1056 |
+
if(node)node.textContent="GPU-Kernel wird zweimal ausgeführt und gegen CPU-Gate-ABI verglichen …";
|
| 1057 |
+
const payload={...cudaRequestPayload(),n_neurons:Math.min(Number(byId("pg-cuda-neurons")?.value||64),4096)};
|
| 1058 |
+
const result=await apiPost("/api/playground/cuda/smoke",payload);
|
| 1059 |
+
const first=result.first||{},ptxas=first.ptxas||{};
|
| 1060 |
+
if(node)node.textContent=JSON.stringify({
|
| 1061 |
+
passed:result.passed,
|
| 1062 |
+
gpu_repeat_exact:result.gpu_repeat_exact,
|
| 1063 |
+
parity:result.parity,
|
| 1064 |
+
launch:first.launch,
|
| 1065 |
+
registers:ptxas.registers,
|
| 1066 |
+
shared_bytes:ptxas.shared_bytes,
|
| 1067 |
+
spill_store_bytes:ptxas.spill_store_bytes,
|
| 1068 |
+
spill_load_bytes:ptxas.spill_load_bytes,
|
| 1069 |
+
cleanup_contract:result.cleanup_contract,
|
| 1070 |
+
},null,2);
|
| 1071 |
+
const resources=byId("pg-cuda-resource-state");
|
| 1072 |
+
if(resources)resources.textContent=JSON.stringify({
|
| 1073 |
+
ptxas:first.ptxas||null,
|
| 1074 |
+
launch:first.launch||null,
|
| 1075 |
+
},null,2);
|
| 1076 |
+
return result;
|
| 1077 |
+
}
|
| 1078 |
+
|
| 1079 |
+
async function runCudaRngParity(){
|
| 1080 |
+
const node=byId("pg-cuda-rng-state");
|
| 1081 |
+
if(node)node.textContent="ε-greedy-Hashpfad wird CPU ↔ CUDA verglichen …";
|
| 1082 |
+
const result=await apiPost("/api/playground/cuda/rng-parity",cudaRequestPayload());
|
| 1083 |
+
if(node)node.textContent=JSON.stringify(result,null,2);
|
| 1084 |
+
return result;
|
| 1085 |
+
}
|
| 1086 |
+
|
| 1087 |
+
async function runCudaRecurrentParity(plasticity=false){
|
| 1088 |
+
plasticity=plasticity===true;
|
| 1089 |
+
const node=byId(plasticity?"pg-cuda-plasticity-state":"pg-cuda-recurrent-state");
|
| 1090 |
+
node.textContent="100 Ticks mit kooperativem Grid und Delay-Ring werden geprüft …";
|
| 1091 |
+
try {
|
| 1092 |
+
const result=await apiPost("/api/playground/cuda/recurrent-parity",{n_neurons:129,ticks:100,block_size:64,plasticity,target_sm:byId("pg-cuda-target-sm")?.value||"sm_86"});
|
| 1093 |
+
node.textContent=JSON.stringify(result,null,2);
|
| 1094 |
+
} catch(error) { node.textContent=String(error.message||error); }
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
async function compileCudaGates(){
|
| 1098 |
const node=byId("pg-cuda-compiler-state");
|
| 1099 |
if(node)node.textContent="Gate-IR/PTX wird erzeugt …";
|
|
|
|
| 1119 |
|
| 1120 |
async function runRobustness(){
|
| 1121 |
const status=byId("pg-status");status.dataset.state="running";status.textContent="Explorative Robustheitskontrollen laufen …";
|
| 1122 |
+
try{const result=await apiPost("/api/playground/robustness",{...formPayload(),persist:false,ensemble_runs:1});status.dataset.state="ok";status.textContent="Robustheitssuite abgeschlossen · nicht preregistriert · keine Evidenz";lastResult=result;byId("pg-run-json").textContent=JSON.stringify(result,null,2);window.MHRNWorkspaceArchitecture?.selectRoute?.("playground","run");}
|
| 1123 |
catch(error){status.dataset.state="error";status.textContent=`Robustheitssuite fehlgeschlagen: ${error.message||error}`;}
|
| 1124 |
}
|
| 1125 |
|
|
|
|
| 1132 |
pan_feedback_gain:"pan-feedback-gain",pan_feedback_nonlinearity:"pan-feedback-nonlinearity",pan_feedback_saturation:"pan-feedback-saturation",
|
| 1133 |
action_loop_enabled:"action-loop-enabled",action_loop_delay:"action-loop-delay",action_persistence:"action-persistence",action_to_input_map:"action-to-input-map",action_space_size:"action-space-size",action_coupling_strength:"action-coupling",action_noise:"action-noise",
|
| 1134 |
reward_signal_enabled:"reward-enabled",reward_magnitude:"reward-magnitude",reward_delay_ticks:"reward-delay",reward_shaping:"reward-shaping",reward_baseline:"reward-baseline",reward_decay:"reward-decay",reward_channel:"reward-channel",
|
| 1135 |
+
target_cue_control:"target-cue-control",target_encoding:"target-encoding",target_cue_channel:"target-cue-channel",target_cue_current:"target-cue-current",target_persistence:"target-persistence",target_shuffle:"target-shuffle",target_predictability:"target-predictability",
|
| 1136 |
credit_assignment:"credit-assignment",credit_window:"credit-window",eligibility_trace_tau:"eligibility-tau",td_lambda:"td-lambda",gamma_discount:"gamma-discount",
|
| 1137 |
neuron_threshold_variance:"threshold-variance",neuron_tau_m_variance:"tau-m-variance",inhibitory_fraction:"inhibitory-fraction",gaba_strength:"gaba-strength",e_i_ratio:"e-i-ratio",delay_distribution:"delay-distribution",delay_mean_ticks:"delay-mean",
|
| 1138 |
refractory_variance:"refractory-variance",adaptation_strength:"adaptation-strength",adaptation_tau:"adaptation-tau",oscillation_enabled:"oscillation-enabled",oscillation_frequency:"oscillation-frequency",
|
| 1139 |
input_topology:"input-topology",input_channels:"input-channels",input_noise_sigma:"input-noise",action_feedback_channel:"action-feedback-channel",reward_cue_channel:"reward-cue-channel",
|
| 1140 |
geometry_input_coupling:"geometry-input-coupling",geometry_input_sigma:"geometry-input-sigma",sandbox_enabled:"sandbox-enabled",sandbox_sensor_noise:"sandbox-sensor-noise",
|
| 1141 |
+
neuron_model:"neuron-model",synapse_model:"synapse-model",plasticity_rule:"plasticity",topology:"topology",readout:"readout",pan_enabled:"pan-enabled",behavior_learning_enabled:"behavior-enabled",stimulus:"stimulus",
|
| 1142 |
+
weight_decay:"weight-decay",weight_max_clamp:"weight-max-clamp",persist:"persist",clock_mode:"clock-mode",clock_base_hz:"clock-base-hz",clock_event_batch_ms:"clock-event-batch",neuron_backend:"neuron-backend",execution_mode:"execution-mode",execution_initial_mode:"execution-initial",execution_theta_high:"execution-high",execution_theta_low:"execution-low",execution_hysteresis:"execution-hysteresis",execution_min_dwell:"execution-dwell",execution_activity_window:"execution-window",execution_transition_mode:"execution-transition",execution_sync_on_switch:"execution-sync",execution_log_transitions:"execution-log",growth_enabled:"growth-enabled",growth_activity_threshold:"growth-activity",growth_coactivation_threshold:"growth-coactivation",growth_information_threshold:"growth-info",growth_prune_threshold:"growth-prune",growth_max_synapses_per_neuron:"growth-max-synapses",growth_max_new_synapses_per_barrier:"growth-max-new",hardware_profile_name:"hardware-profile",cuda_budget_mb:"cuda-budget",offload_enabled:"offload-enabled",offload_snapshot_interval:"offload-snapshot",thalamic_gating_enabled:"thalamic-enabled",thalamic_relay_threshold:"thalamic-threshold",thalamic_attention_gain:"thalamic-attention",thalamic_inhibition_gain:"thalamic-inhibition",cortical_layers_enabled:"cortical-enabled",cortical_plasticity:"cortical-plasticity",cortical_learning_rate:"cortical-lr",behavior_target_mode:"behavior-target-mode",behavior_min_activity:"behavior-min-activity",behavior_learning_rate:"behavior-lr",behavior_epsilon:"behavior-epsilon",geometry_lambda_a:"geometry-lambda-a",geometry_lambda_b:"geometry-lambda-b",geometry_sigma:"geometry-sigma",geometry_p0:"geometry-p0",geometry_mode:"geometry-mode",geometry_delay_velocity:"geometry-delay-velocity",neural_io_enabled:"neural-io-enabled",neural_io_input_channels:"neural-io-input-channels",neural_io_output_channels:"neural-io-output-channels",neural_io_input_codec:"neural-io-codec",neural_io_output_decoder:"neural-io-decoder",neural_io_window_ticks:"neural-io-window",neural_io_input_current:"neural-io-current",neural_io_input_role:"neural-io-input-role",neural_io_output_role:"neural-io-output-role",neural_io_phase:"neural-io-phase",neural_io_modality:"neural-io-modality",neural_io_source_id:"neural-io-source",posture_score_channel:"posture-score-channel",reward_event_channel:"reward-event-channel",posture_current_scale:"posture-current-scale",reward_event_scale:"reward-event-scale",posture_reward_enabled:"posture-reward-enabled",posture_weight_upright:"posture-weight-upright",posture_weight_height:"posture-weight-height",posture_weight_stability:"posture-weight-stability",posture_weight_symmetry:"posture-weight-symmetry",posture_target_height:"posture-target-height",posture_tilt_max:"posture-tilt-max",posture_velocity_max:"posture-velocity-max",trigger_good_score:"trigger-good-score",trigger_good_duration:"trigger-good-duration",trigger_good_reward:"trigger-good-reward",trigger_warning_score:"trigger-warning-score",trigger_warning_reward:"trigger-warning-reward",trigger_falling_rate:"trigger-falling-rate",trigger_falling_reward:"trigger-falling-reward",trigger_collapse_score:"trigger-collapse-score",trigger_collapse_reward:"trigger-collapse-reward",trigger_recovery_bonus:"trigger-recovery-bonus",reward_continuous_alpha:"reward-continuous-alpha",episode_termination_enabled:"episode-termination-enabled",episode_max_ticks:"episode-max-ticks",episode_reset_on_collapse:"episode-reset-on-collapse",pan_feedback_delay:"pan-feedback-delay",pan_feedback_source:"pan-feedback-source",pan_feedback_target:"pan-feedback-target",pan_feedback_threshold:"pan-feedback-threshold",freeze_actions:"freeze-actions",freeze_rewards:"freeze-rewards",parity_reference_commit:"parity-reference-commit"
|
| 1143 |
};
|
| 1144 |
+
if(key==="neural_io_input_payload"){const field=byId("pg-neural-io-payload");if(field)field.value=typeof value==="string"?value:JSON.stringify(value);return;}
|
| 1145 |
const id="pg-"+(aliases[key]||key.replaceAll("_","-"));
|
| 1146 |
const el=byId(id);if(!el)return;
|
| 1147 |
if(el.type==="checkbox")el.checked=Boolean(value);
|
|
|
|
| 1163 |
const select=byId("pg-user-preset-select");if(!select)return;
|
| 1164 |
const selected=select.value;
|
| 1165 |
const presets=allPlaygroundPresets();
|
| 1166 |
+
select.innerHTML=Object.entries(presets).map(([name,preset])=>`<option value="${playgroundEscape(name)}">${playgroundEscape(preset.label||name)} · ${preset.source==="catalog"?"Katalog":BUILTIN_PLAYGROUND_PRESETS[name]?"integriert":"Browser"}</option>`).join("");
|
| 1167 |
if(selected&&presets[selected])select.value=selected;else if(!select.value)select.value=presets[DEFAULT_PLAYGROUND_PRESET]?DEFAULT_PLAYGROUND_PRESET:"pan_exploration";
|
| 1168 |
const preset=presets[select.value];
|
| 1169 |
if(byId("pg-user-preset-description"))byId("pg-user-preset-description").textContent=preset?.description||"Preset auswählen oder eigene Einstellungen speichern.";
|
|
|
|
| 1230 |
}
|
| 1231 |
|
| 1232 |
function showCatalogInfo(item){
|
| 1233 |
+
activeCatalogInfo=item;const language=catalogLanguage();const dialog=ensureCatalogInfoDialog();const categoryKey=item.categoryKey||item.category||"repo";const category=catalogCategoryTitle(categoryKey,language);const description=describeCatalogItem(item,category,language);const execution=catalogExecutionLabel(item,categoryKey,language);byId("pg-catalog-info-category").textContent=category;byId("pg-catalog-info-title").textContent=item.label||item.name||"—";byId("pg-catalog-info-body").innerHTML=`<p><strong>${language==="de"?"Ausführbarkeit":"Execution"}:</strong> ${playgroundEscape(execution)}</p><p>${playgroundEscape(description)}</p>${item.source?`<p><strong>${language==="de"?"Quelle":"Source"}:</strong> <code>${playgroundEscape(item.source)}</code></p>`:""}<p><strong>${language==="de"?"Grenze":"Boundary"}:</strong> ${language==="de"?"Explorativ. Der Katalogeintrag ist keine Instanzierung und erzeugt keine wissenschaftliche Evidenz.":"Exploratory. This catalog entry is not an instantiation and does not create scientific evidence."}</p>`;dialog.querySelector("#pg-catalog-info-close").setAttribute("aria-label",language==="de"?"Bausteininfo schließen":"Close component information");dialog.showModal();
|
| 1234 |
}
|
| 1235 |
|
| 1236 |
function bindCatalogInfo(){
|
| 1237 |
const grid=byId("pg-catalog-grid");if(!grid||grid.dataset.infoBound)return;grid.dataset.infoBound="true";grid.addEventListener("click",event=>{const button=event.target.closest("[data-pg-catalog-info]");if(!button)return;const item=catalogInfoItems.get(button.dataset.pgCatalogInfo);if(item)showCatalogInfo(item);});
|
| 1238 |
}
|
| 1239 |
|
| 1240 |
+
function catalogExecutionLabel(item, categoryKey, language=catalogLanguage()){
|
| 1241 |
+
const nonExecutableCategory=["panCandidates","panLiterature","geometryLiterature","neuralIO","analyses","robustness","cudaCompiler","cudaParity","repo"].includes(categoryKey);
|
| 1242 |
+
const minimum=Number(item.min_dimensions||0);const dimensions=Number(byId("pg-dimensions")?.value||5);
|
| 1243 |
+
if (item.executable===false||nonExecutableCategory) return language==="de"?"Nur Katalog / nicht direkt ausführbar":"Catalog-only / not directly executable";
|
| 1244 |
+
if (minimum>0&&dimensions<minimum) return language==="de"?`Nicht ausführbar bei ${dimensions}D; benötigt ${minimum}D`:`Not executable at ${dimensions}D; requires ${minimum}D`;
|
| 1245 |
+
return language==="de"?"Ausführbarer Referenzpfad":"Executable reference path";
|
| 1246 |
+
}
|
| 1247 |
+
|
| 1248 |
function renderCatalogCards(catalog){
|
| 1249 |
const panCandidates=(catalog.pan?.research_candidates||[]).map(item=>({name:item.id,note:item.question}));
|
| 1250 |
const panLiterature=(catalog.pan?.literature_context?.sources||[]).map(item=>({name:item.key,note:item.citation}));
|
|
|
|
| 1252 |
const neuralIOCodecs=(catalog.neural_io?.codecs||[]).map(item=>({name:item.id,note:`${item.input_kind||""} · ${item.reconstruction_class||""}`}));
|
| 1253 |
const parityClasses=Object.entries(catalog.cuda_parity?.classes||{}).map(([name,item])=>({name,note:item.name||item.target_stage||""}));
|
| 1254 |
const groups=[["models",catalog.models],["topologies",catalog.topologies],["stimuli",catalog.stimuli],["synapses",catalog.synapses],["plasticity",catalog.plasticity],["readouts",catalog.readouts],["panCandidates",panCandidates],["panLiterature",panLiterature],["geometryLiterature",geometryLiterature],["neuralIO",neuralIOCodecs],["analyses",(catalog.analyses||[]).map(name=>({name}))],["robustness",(catalog.robustness_controls||[]).map(name=>({name}))],["cudaCompiler",(catalog.cuda_gate_compiler?.gate_types||[]).map(name=>({name,note:catalog.cuda_gate_compiler?.status||""}))],["cudaParity",parityClasses]];
|
| 1255 |
+
groups.push(["repo",PLAYGROUND_REPO_ELEMENTS.map(([name,category,description,source])=>({name,label:name,note:description,category,source,executable:false}))]);
|
| 1256 |
catalogInfoItems=new Map();let itemIndex=0;const language=catalogLanguage();
|
| 1257 |
+
byId("pg-catalog-grid").innerHTML=groups.map(([categoryKey,items])=>{const title=catalogCategoryTitle(categoryKey,language);return `<article><h3>${playgroundEscape(title)}</h3>${(items||[]).map(item=>{const infoKey=String(itemIndex++);const info={...item,categoryKey};catalogInfoItems.set(infoKey,info);const description=describeCatalogItem(info,title,language);const execution=catalogExecutionLabel(info,categoryKey,language);return `<button type="button" class="playground-chip" data-pg-catalog-info="${infoKey}" title="${playgroundEscape(`${execution}. ${description}`)}">${playgroundEscape(item.label||item.name)}</button>`;}).join("")}</article>`;}).join("");
|
| 1258 |
}
|
| 1259 |
|
| 1260 |
function renderCatalog(catalog){
|
|
|
|
| 1272 |
const status=byId("pg-status");status.dataset.state="ok";status.textContent=`Bereit · bis ${catalog.limits.n_neurons} Neuronen · ${catalog.limits.edges} Kanten · ${catalog.limits.dimensions}D · scientific_evidence=false`;
|
| 1273 |
}
|
| 1274 |
|
| 1275 |
+
|
| 1276 |
+
function integrationStatusLabel(status){
|
| 1277 |
+
return {
|
| 1278 |
+
INTEGRATED:"In MHRN integriert",
|
| 1279 |
+
READY_FOR_NEXT_WAVE:"Bereit für nächste Welle",
|
| 1280 |
+
READY_FOR_WAVE4:"Bereit für Wave 4",
|
| 1281 |
+
READY_AFTER_BACKEND_CONTRACT:"Backend-Vertrag zuerst",
|
| 1282 |
+
BLOCKED_CONTRACT_FREEZE:"Contract-Freeze erforderlich",
|
| 1283 |
+
BLOCKED_FROZEN_ENVIRONMENT:"Frozen-Environment erforderlich",
|
| 1284 |
+
FE_CONTRACT_EXECUTABLE_CUDA_D3_PENDING:"FE-Vertrag ausführbar · CUDA-D3 offen",
|
| 1285 |
+
FE_CONTRACT_EXECUTABLE_BUILDER_D3_BRIDGE_AVAILABLE_FE3_PENDING:"FE-Vertrag ausführbar · Builder-D3c Hardware-Bridge verfügbar · FE-3 Live-Adapter offen",
|
| 1286 |
+
BLOCKED_PAN_SEMANTICS:"PAN-Semantik erforderlich",
|
| 1287 |
+
SEMANTIC_CONTRACT_DRAFT:"Semantikvertrag in Arbeit",
|
| 1288 |
+
WAVE6_CONTRACT_DRAFT:"Wave 6 · Structural-Approval-Draft",
|
| 1289 |
+
WAVE7_CONTRACT_DRAFT:"Wave 7 · Learning-Contract-Draft",
|
| 1290 |
+
RETAINED_NOT_CORE:"Unter OLD behalten",
|
| 1291 |
+
FE3_LIVE_ADAPTER_IMPLEMENTED_HARDWARE_ACCEPTANCE_PENDING:"FE-3 Adapter integriert · Hardware-Abnahme offen",
|
| 1292 |
+
}[status]||status;
|
| 1293 |
+
}
|
| 1294 |
+
|
| 1295 |
+
function ensureMhrnIntegrationPanel(){
|
| 1296 |
+
const builder=byId("playground-builder");
|
| 1297 |
+
if(!builder||byId("pg-mhrn-integration"))return;
|
| 1298 |
+
const panel=document.createElement("section");
|
| 1299 |
+
panel.id="pg-mhrn-integration";
|
| 1300 |
+
panel.className="playground-card pg-wide";
|
| 1301 |
+
panel.innerHTML=`
|
| 1302 |
+
<h3>MHRN Integration</h3>
|
| 1303 |
+
<p>Playground-Bausteine kontrolliert in die kanonische MHRN-Struktur übernehmen. Der Transfer erzeugt keine DATA/EVID und verändert niemals Repository-Quellcode zur Laufzeit.</p>
|
| 1304 |
+
<div class="pg-summary"><strong>Richtung:</strong> Playground → MHRN. MHRN importiert niemals Playground.</div>
|
| 1305 |
+
<div id="pg-mhrn-integration-list" class="playground-analysis-grid"><p>Integrationsstatus wird geladen …</p></div>
|
| 1306 |
+
<pre id="pg-mhrn-integration-state" class="pg-config-preview">Noch keine Transferprüfung ausgeführt.</pre>
|
| 1307 |
+
`;
|
| 1308 |
+
const grid=builder.querySelector(".playground-grid");
|
| 1309 |
+
if(grid)grid.before(panel);else builder.append(panel);
|
| 1310 |
+
}
|
| 1311 |
+
|
| 1312 |
+
function renderIntegrationCatalog(catalog){
|
| 1313 |
+
const root=byId("pg-mhrn-integration-list");if(!root)return;
|
| 1314 |
+
const candidates=catalog?.candidates||[];
|
| 1315 |
+
root.innerHTML=candidates.map(item=>`
|
| 1316 |
+
<article class="playground-analysis-card" data-pg-integration-card="${playgroundEscape(item.element_id)}">
|
| 1317 |
+
<h4>${playgroundEscape(item.label)}</h4>
|
| 1318 |
+
<p><strong>${playgroundEscape(integrationStatusLabel(item.status))}</strong></p>
|
| 1319 |
+
<p>${playgroundEscape(item.notes||"")}</p>
|
| 1320 |
+
<small>Quelle: ${playgroundEscape((item.source_paths||[]).join(", ")||"—")}<br>Ziel: ${playgroundEscape((item.target_paths||[]).join(", ")||"OLD / kein Core-Ziel")}<br>Nächstes Gate: ${playgroundEscape(item.next_gate||"—")}</small>
|
| 1321 |
+
${(item.old_routes||[]).length?`<p><small>OLD: ${playgroundEscape(item.old_routes.join(", "))}</small></p>`:""}
|
| 1322 |
+
<button type="button" data-pg-transfer="${playgroundEscape(item.element_id)}">${item.status==="INTEGRATED"?"Übertragung prüfen":"Übernahme vorbereiten"}</button>
|
| 1323 |
+
</article>
|
| 1324 |
+
`).join("");
|
| 1325 |
+
}
|
| 1326 |
+
|
| 1327 |
+
async function refreshIntegrationCatalog(){
|
| 1328 |
+
ensureMhrnIntegrationPanel();
|
| 1329 |
+
const catalog=await apiGet("/api/playground/integration");
|
| 1330 |
+
renderIntegrationCatalog(catalog);
|
| 1331 |
+
return catalog;
|
| 1332 |
+
}
|
| 1333 |
+
|
| 1334 |
+
async function transferIntegrationElement(elementId){
|
| 1335 |
+
const state=byId("pg-mhrn-integration-state");
|
| 1336 |
+
if(state)state.textContent=`${elementId}: Transfer-Gate wird geprüft …`;
|
| 1337 |
+
try{
|
| 1338 |
+
const result=await apiPost("/api/playground/integration/transfer",{element_id:elementId});
|
| 1339 |
+
if(state)state.textContent=JSON.stringify(result,null,2);
|
| 1340 |
+
await refreshIntegrationCatalog();
|
| 1341 |
+
return result;
|
| 1342 |
+
}catch(error){
|
| 1343 |
+
if(state)state.textContent=`Transferprüfung fehlgeschlagen: ${error.message||error}`;
|
| 1344 |
+
throw error;
|
| 1345 |
+
}
|
| 1346 |
+
}
|
| 1347 |
+
|
| 1348 |
export async function initPlayground(){
|
| 1349 |
const root=byId("tab-playground");if(!root)return;
|
| 1350 |
+
injectStyles();ensurePermanentBoundary(root);buildPanels(root);ensureMhrnIntegrationPanel();
|
| 1351 |
+
root.addEventListener("click",event=>{const button=event.target.closest("[data-pg-transfer]");if(button)transferIntegrationElement(button.dataset.pgTransfer).catch(()=>{});});
|
| 1352 |
+
byId("pg-synaptic-transfer")?.addEventListener("click",async()=>{
|
| 1353 |
+
const summary=byId("pg-transfer-summary"),node=byId("pg-transfer-state");summary.textContent="Vortraining und zwei gepaarte Transferlaeufe ...";
|
| 1354 |
+
try{const result=await apiPost("/api/playground/research/synaptic-transfer",formPayload());node.textContent=JSON.stringify(result,null,2);summary.textContent=`Vergleich abgeschlossen · ${result.changed_pretraining_weights} veraenderte Gewichte. Externer Aktivitaetsdecoder; kein automatischer Nachweis schnellerer neuronaler Aufgabenloesung.`;}catch(error){summary.textContent=`Transfer-Vergleich fehlgeschlagen: ${error.message}`;}
|
| 1355 |
+
});
|
| 1356 |
+
byId("pg-cue-controls")?.addEventListener("click",async()=>{
|
| 1357 |
+
const node=byId("pg-cue-controls-state");node.textContent="Sechs gepaarte Kontrolllaeufe ...";
|
| 1358 |
+
try{node.textContent=JSON.stringify(await apiPost("/api/playground/research/cue-controls",formPayload()),null,2);}catch(error){node.textContent=`Cue-Kontrollen fehlgeschlagen: ${error.message}`;}
|
| 1359 |
+
});
|
| 1360 |
+
byId("pg-cuda-builder-parity")?.addEventListener("click",async()=>{
|
| 1361 |
+
const node=byId("pg-cuda-builder-parity-state");node.textContent="CPU- und CUDA-Builder laufen ...";
|
| 1362 |
+
try{node.textContent=JSON.stringify(await apiPost("/api/playground/cuda/builder-parity",formPayload()),null,2);}catch(error){node.textContent=`Builder-Paritaet fehlgeschlagen: ${error.message}`;}
|
| 1363 |
+
});
|
| 1364 |
+
byId("pg-cuda-recurrent")?.addEventListener("click",runCudaRecurrentParity);
|
| 1365 |
+
byId("pg-cuda-plasticity")?.addEventListener("click",()=>runCudaRecurrentParity(true));
|
| 1366 |
if(!root.dataset.languageBound){root.dataset.languageBound="true";document.addEventListener("mhrn:language-change",()=>{if(catalogState)renderCatalogCards(catalogState);const dialog=byId("pg-catalog-info-dialog");if(dialog?.open&&activeCatalogInfo)showCatalogInfo(activeCatalogInfo);});}
|
| 1367 |
+
byId("pg-run")?.addEventListener("click",runSession);byId("pg-cuda-status")?.addEventListener("click",()=>refreshCudaStatus().catch(error=>{const node=byId("pg-cuda-stage-state");if(node)node.textContent=String(error.message||error);}));byId("pg-cpu-determinism")?.addEventListener("click",()=>checkCpuDeterminism().catch(error=>{const node=byId("pg-cuda-compiler-state");if(node)node.textContent=String(error.message||error);}));byId("pg-cuda-compile")?.addEventListener("click",()=>compileCudaGates().catch(error=>{const node=byId("pg-cuda-compiler-state");if(node)node.textContent=String(error.message||error);}));byId("pg-cuda-preflight")?.addEventListener("click",()=>runCudaPreflight().catch(error=>{const node=byId("pg-cuda-resource-state");if(node)node.textContent=String(error.message||error);}));byId("pg-cuda-smoke")?.addEventListener("click",()=>runCudaHardwareSmoke().catch(error=>{const node=byId("pg-cuda-parity-state");if(node)node.textContent=String(error.message||error);}));byId("pg-cuda-rng")?.addEventListener("click",()=>runCudaRngParity().catch(error=>{const node=byId("pg-cuda-rng-state");if(node)node.textContent=String(error.message||error);}));byId("pg-user-preset-apply")?.addEventListener("click",applyUserPreset);byId("pg-user-preset-save")?.addEventListener("click",saveUserPreset);byId("pg-user-preset-delete")?.addEventListener("click",deleteUserPreset);byId("pg-user-preset-select")?.addEventListener("change",()=>{renderUserPresetOptions();applyUserPreset();});byId("pg-robustness")?.addEventListener("click",runRobustness);byId("pg-reset")?.addEventListener("click",resetForm);byId("pg-live-open")?.addEventListener("click",()=>openLiveMonitor().catch(error=>byId("pg-live-state").textContent=String(error.message||error)));byId("pg-live-clear")?.addEventListener("click",()=>clearLiveSessions().catch(error=>byId("pg-live-state").textContent=String(error.message||error)));byId("pg-night-start")?.addEventListener("click",()=>startNightRun().catch(error=>byId("pg-night-state").textContent=String(error.message||error)));byId("pg-night-stop")?.addEventListener("click",()=>stopNightRun().catch(error=>byId("pg-night-state").textContent=String(error.message||error)));byId("pg-night-refresh")?.addEventListener("click",()=>refreshNightStatus().catch(error=>byId("pg-night-state").textContent=String(error.message||error)));
|
| 1368 |
renderUserPresetOptions();
|
| 1369 |
try{renderCatalog(await apiGet("/api/playground/catalog"));renderUserPresetOptions();resetForm();}catch(error){const status=byId("pg-status");status.dataset.state="error";status.textContent=`Katalog nicht verfügbar: ${error.message}`;}
|
| 1370 |
+
try{await refreshIntegrationCatalog();}catch(error){const state=byId("pg-mhrn-integration-state");if(state)state.textContent=`Integrationsstatus nicht verfügbar: ${error.message}`;}
|
| 1371 |
byId("playground-builder")?.addEventListener("input", updateDefaultHints);
|
| 1372 |
byId("playground-builder")?.addEventListener("change", updateDefaultHints);
|
| 1373 |
+
initGuidedWorkspace({payload:formPayload,presets:()=>Object.fromEntries(Object.entries(allPlaygroundPresets()).map(([name,preset])=>[name,{...preset,settings:preset.source==="catalog"?resolvedPresetSettings(name):preset.settings}])),applyPreset:applyUserPreset,setValue:setBuilderValue,lastResult:()=>lastResult});
|
| 1374 |
await refreshSessions();
|
| 1375 |
try{await refreshNightStatus();}catch{ /* night manager is optional during partial deployments */ }
|
| 1376 |
+
try{await refreshCudaStatus();}catch{ /* CUDA toolkit/driver is optional */ }
|
| 1377 |
+
window.MHRNPlayground={run:runSession,runRobustness,checkCpuDeterminism,compileCudaGates,refreshCudaStatus,runCudaPreflight,runCudaHardwareSmoke,runCudaRngParity,refreshSessions,refreshIntegrationCatalog,transferIntegrationElement,get catalog(){return catalogState;},get lastResult(){return lastResult;}};
|
| 1378 |
}
|
src/dashboard/static/frontend/modules/recurrent-snn-stage.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
| 1 |
"use strict";
|
| 2 |
|
| 3 |
-
const ROUTES = Object.freeze({
|
| 4 |
-
|
| 5 |
-
wesen: ["recurrent", "Rekurrenz", "wesen", "focus", "#mhrn-runtime-recurrent"],
|
| 6 |
-
control: ["recurrent", "Rekurrenz-Parameter", "settings", "focus", "#mhrn-recurrent-control"],
|
| 7 |
-
});
|
| 8 |
|
| 9 |
const EDITABLE = Object.freeze([
|
| 10 |
"network.initial_connections_per_neuron",
|
|
@@ -102,7 +99,7 @@ function ensureSciencePanel() {
|
|
| 102 |
<div id="recurrent-science-summary" class="small-snn-summary"></div>
|
| 103 |
<div class="small-snn-grid">
|
| 104 |
<section><h3>Live-Netz</h3><div id="recurrent-science-graph" class="small-snn-graph"></div></section>
|
| 105 |
-
<section><h3>Stage-2 Contract</h3><div class="small-snn-contract"><strong>A → B → C → D → A</strong><span>20.000 ticks reference run</span><span>bounded event queue</span><span>deterministic replay identity</span><span>restart/restore independently verified</span><span>RQ-SNN-001: 100.000 ticks/run research DATA</span></div><div class="small-snn-actions"><button type="button" data-route-jump="
|
| 106 |
</div>
|
| 107 |
<div id="recurrent-science-status" class="small-snn-status">lade …</div>`;
|
| 108 |
root.appendChild(panel);
|
|
@@ -114,7 +111,7 @@ function ensureRuntimePanel() {
|
|
| 114 |
const panel = document.createElement("section");
|
| 115 |
panel.id = "mhrn-runtime-recurrent";
|
| 116 |
panel.className = "card small-snn-panel runtime-snn-panel";
|
| 117 |
-
panel.dataset.mhrnRoute = "
|
| 118 |
panel.hidden = true;
|
| 119 |
panel.innerHTML = `
|
| 120 |
<header class="small-snn-head"><div><span class="workspace-kicker">RUNTIME & WESEN · STAGE 2</span><h2>Rekurrenz Live</h2><p>Read-only Beobachtung des real laufenden Netzwerks mit Tick, Aktivität, Queue und rückgekoppelten Kanten im sichtbaren Ausschnitt.</p></div><button id="recurrent-runtime-refresh" type="button">↻ Aktualisieren</button></header>
|
|
@@ -132,12 +129,12 @@ function ensureControlPanel() {
|
|
| 132 |
const panel = document.createElement("section");
|
| 133 |
panel.id = "mhrn-recurrent-control";
|
| 134 |
panel.className = "card small-snn-panel";
|
| 135 |
-
panel.dataset.mhrnRoute = "
|
| 136 |
panel.hidden = true;
|
| 137 |
panel.innerHTML = `
|
| 138 |
<header class="small-snn-head"><div><span class="workspace-kicker">CONTROL · STAGE 2</span><h2>Rekurrenz-Parameter</h2><p>Relevante Konstruktionsparameter werden ausschließlich als Pending Changes vorbereitet.</p></div><span id="recurrent-pending" class="small-snn-stage-badge">0 pending</span></header>
|
| 139 |
<div id="recurrent-control-fields" class="small-snn-control-fields"></div>
|
| 140 |
-
<div class="small-snn-actions"><button id="recurrent-stage" type="button">Stage changes</button><button id="recurrent-apply" type="button" class="btn-primary">Apply</button><button type="button" data-route-jump="
|
| 141 |
<div id="recurrent-control-status" class="small-snn-status">lade …</div>
|
| 142 |
<div class="small-snn-boundary">Topologie-, Radius- und Refraktäränderungen sind wissenschaftlich sensitiv. Ein neuer Run/Restart ist erforderlich.</div>`;
|
| 143 |
root.appendChild(panel);
|
|
@@ -228,7 +225,7 @@ function summaryHtml(data) {
|
|
| 228 |
}
|
| 229 |
|
| 230 |
async function refreshScience() {
|
| 231 |
-
if (document.body.dataset.currentArea !== "
|
| 232 |
try {
|
| 233 |
const data = await loadNetwork();
|
| 234 |
$("recurrent-science-summary").innerHTML = summaryHtml(data);
|
|
@@ -241,8 +238,8 @@ async function refreshScience() {
|
|
| 241 |
|
| 242 |
async function refreshRuntime() {
|
| 243 |
if (
|
| 244 |
-
document.body.dataset.currentArea !== "
|
| 245 |
-
document.body.dataset.currentRoute !== "recurrent" ||
|
| 246 |
state.inFlight
|
| 247 |
) return;
|
| 248 |
state.inFlight = true;
|
|
@@ -284,7 +281,7 @@ function renderControl() {
|
|
| 284 |
}
|
| 285 |
|
| 286 |
async function refreshControl() {
|
| 287 |
-
if (document.body.dataset.currentArea !== "
|
| 288 |
try {
|
| 289 |
const [parameters, pending] = await Promise.all([
|
| 290 |
json("/api/parameters"),
|
|
@@ -342,17 +339,17 @@ async function applyControl() {
|
|
| 342 |
function routeRefresh() {
|
| 343 |
const area = document.body.dataset.currentArea;
|
| 344 |
const route = document.body.dataset.currentRoute;
|
| 345 |
-
if (
|
| 346 |
-
if (
|
| 347 |
-
if (
|
| 348 |
-
if (
|
| 349 |
}
|
| 350 |
|
| 351 |
function syncPolling() {
|
| 352 |
const area = document.body.dataset.currentArea;
|
| 353 |
const route = document.body.dataset.currentRoute;
|
| 354 |
-
const liveRoute =
|
| 355 |
-
if (
|
| 356 |
if (liveRoute && !state.timer) {
|
| 357 |
state.timer = setInterval(routeRefresh, 1000);
|
| 358 |
return;
|
|
@@ -374,7 +371,7 @@ export function initRecurrentSNNStage() {
|
|
| 374 |
attributeFilter: ["data-current-area", "data-current-route"],
|
| 375 |
});
|
| 376 |
document.addEventListener("click", (event) => {
|
| 377 |
-
if (event.target.closest('[data-area-route="recurrent"], [data-route-card="recurrent"]')) {
|
| 378 |
setTimeout(syncPolling, 0);
|
| 379 |
}
|
| 380 |
});
|
|
|
|
| 1 |
"use strict";
|
| 2 |
|
| 3 |
+
const ROUTES = Object.freeze({});
|
| 4 |
+
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
const EDITABLE = Object.freeze([
|
| 7 |
"network.initial_connections_per_neuron",
|
|
|
|
| 99 |
<div id="recurrent-science-summary" class="small-snn-summary"></div>
|
| 100 |
<div class="small-snn-grid">
|
| 101 |
<section><h3>Live-Netz</h3><div id="recurrent-science-graph" class="small-snn-graph"></div></section>
|
| 102 |
+
<section><h3>Stage-2 Contract</h3><div class="small-snn-contract"><strong>A → B → C → D → A</strong><span>20.000 ticks reference run</span><span>bounded event queue</span><span>deterministic replay identity</span><span>restart/restore independently verified</span><span>RQ-SNN-001: 100.000 ticks/run research DATA</span></div><div class="small-snn-actions"><button type="button" data-route-jump="old:wesen-recurrent">Runtime ansehen</button><button type="button" data-route-jump="old:control-recurrent">Rekurrenz-Parameter</button><button type="button" data-route-jump="release:development">Release-Entwicklung</button></div></section>
|
| 103 |
</div>
|
| 104 |
<div id="recurrent-science-status" class="small-snn-status">lade …</div>`;
|
| 105 |
root.appendChild(panel);
|
|
|
|
| 111 |
const panel = document.createElement("section");
|
| 112 |
panel.id = "mhrn-runtime-recurrent";
|
| 113 |
panel.className = "card small-snn-panel runtime-snn-panel";
|
| 114 |
+
panel.dataset.mhrnRoute = "old:wesen-recurrent";
|
| 115 |
panel.hidden = true;
|
| 116 |
panel.innerHTML = `
|
| 117 |
<header class="small-snn-head"><div><span class="workspace-kicker">RUNTIME & WESEN · STAGE 2</span><h2>Rekurrenz Live</h2><p>Read-only Beobachtung des real laufenden Netzwerks mit Tick, Aktivität, Queue und rückgekoppelten Kanten im sichtbaren Ausschnitt.</p></div><button id="recurrent-runtime-refresh" type="button">↻ Aktualisieren</button></header>
|
|
|
|
| 129 |
const panel = document.createElement("section");
|
| 130 |
panel.id = "mhrn-recurrent-control";
|
| 131 |
panel.className = "card small-snn-panel";
|
| 132 |
+
panel.dataset.mhrnRoute = "old:control-recurrent";
|
| 133 |
panel.hidden = true;
|
| 134 |
panel.innerHTML = `
|
| 135 |
<header class="small-snn-head"><div><span class="workspace-kicker">CONTROL · STAGE 2</span><h2>Rekurrenz-Parameter</h2><p>Relevante Konstruktionsparameter werden ausschließlich als Pending Changes vorbereitet.</p></div><span id="recurrent-pending" class="small-snn-stage-badge">0 pending</span></header>
|
| 136 |
<div id="recurrent-control-fields" class="small-snn-control-fields"></div>
|
| 137 |
+
<div class="small-snn-actions"><button id="recurrent-stage" type="button">Stage changes</button><button id="recurrent-apply" type="button" class="btn-primary">Apply</button><button type="button" data-route-jump="settings:parameters">Alle Parameter</button><button type="button" data-route-jump="old:science-recurrent">Wissenschaft</button></div>
|
| 138 |
<div id="recurrent-control-status" class="small-snn-status">lade …</div>
|
| 139 |
<div class="small-snn-boundary">Topologie-, Radius- und Refraktäränderungen sind wissenschaftlich sensitiv. Ein neuer Run/Restart ist erforderlich.</div>`;
|
| 140 |
root.appendChild(panel);
|
|
|
|
| 225 |
}
|
| 226 |
|
| 227 |
async function refreshScience() {
|
| 228 |
+
if (document.body.dataset.currentArea !== "old" || document.body.dataset.currentRoute !== "science-recurrent") return;
|
| 229 |
try {
|
| 230 |
const data = await loadNetwork();
|
| 231 |
$("recurrent-science-summary").innerHTML = summaryHtml(data);
|
|
|
|
| 238 |
|
| 239 |
async function refreshRuntime() {
|
| 240 |
if (
|
| 241 |
+
document.body.dataset.currentArea !== "old" ||
|
| 242 |
+
document.body.dataset.currentRoute !== "wesen-recurrent" ||
|
| 243 |
state.inFlight
|
| 244 |
) return;
|
| 245 |
state.inFlight = true;
|
|
|
|
| 281 |
}
|
| 282 |
|
| 283 |
async function refreshControl() {
|
| 284 |
+
if (document.body.dataset.currentArea !== "old" || document.body.dataset.currentRoute !== "control-recurrent") return;
|
| 285 |
try {
|
| 286 |
const [parameters, pending] = await Promise.all([
|
| 287 |
json("/api/parameters"),
|
|
|
|
| 339 |
function routeRefresh() {
|
| 340 |
const area = document.body.dataset.currentArea;
|
| 341 |
const route = document.body.dataset.currentRoute;
|
| 342 |
+
if (area !== "old") return;
|
| 343 |
+
if (route === "science-recurrent") refreshScience();
|
| 344 |
+
if (route === "wesen-recurrent") refreshRuntime();
|
| 345 |
+
if (route === "control-recurrent") refreshControl();
|
| 346 |
}
|
| 347 |
|
| 348 |
function syncPolling() {
|
| 349 |
const area = document.body.dataset.currentArea;
|
| 350 |
const route = document.body.dataset.currentRoute;
|
| 351 |
+
const liveRoute = area === "old" && (route === "science-recurrent" || route === "wesen-recurrent");
|
| 352 |
+
if (area === "old" && route.endsWith("-recurrent")) routeRefresh();
|
| 353 |
if (liveRoute && !state.timer) {
|
| 354 |
state.timer = setInterval(routeRefresh, 1000);
|
| 355 |
return;
|
|
|
|
| 371 |
attributeFilter: ["data-current-area", "data-current-route"],
|
| 372 |
});
|
| 373 |
document.addEventListener("click", (event) => {
|
| 374 |
+
if (event.target.closest('[data-area-route="science-recurrent"], [data-area-route="wesen-recurrent"], [data-area-route="control-recurrent"], [data-route-card="science-recurrent"], [data-route-card="wesen-recurrent"], [data-route-card="control-recurrent"]')) {
|
| 375 |
setTimeout(syncPolling, 0);
|
| 376 |
}
|
| 377 |
});
|