# LightGlueRuntime Helper Module (R14 fix) **Task**: AZ-278_lightglue_runtime **Name**: LightGlueRuntime Helper **Description**: Implement the shared `LightGlueRuntime` helper that owns the LightGlue inference engine handle for both C2.5 (single-pair inlier counting) and C3 (heavier matching pass). This is the structural fix for R14 (the original C2.5 ↔ C3 import cycle): the runtime sits at Layer 1 with no `components.*` imports, so the cycle becomes impossible to express. Single CUDA stream; concurrent access forbidden by contract; composition root binds to the single F3 hot-path thread. **Complexity**: 3 points **Dependencies**: AZ-263_initial_structure **Component**: shared.helpers.lightglue_runtime (cross-cutting; epic AZ-264 / E-CC-HELPERS) **Tracker**: AZ-278 **Epic**: AZ-264 (E-CC-HELPERS) ### Document Dependencies - `_docs/02_document/contracts/shared_helpers/lightglue_runtime.md` — frozen public interface this task produces. - `_docs/02_document/common-helpers/03_helper_lightglue_runtime.md` — design rationale and R14 context. ## Problem C2.5 (Re-rank) and C3 (CrossDomainMatcher) both call LightGlue. In cycle 1 of `_docs/02_document/epics.md`, LightGlue ownership was ambiguous and produced R14: a circular import / runtime dependency between C2.5 and C3 (the "K=10 → N=3 funnel" both wanted to own the engine). Without a shared runtime: - The engine is built / loaded twice, doubling GPU memory at takeoff (Tier-2 has only 8 GB). - C2.5 and C3 drift on engine version pinning, producing inconsistent matches. - Their import cycle is a recurring footgun: any future refactor will tempt one to import from the other. ## Outcome - A single `LightGlueRuntime` instance is constructed once at takeoff by the composition root from C7's `deserialize_engine(LIGHTGLUE_ENGINE_CACHE_ENTRY)` and is constructor-injected into BOTH C2.5 and C3. - The C2.5 ↔ C3 import cycle is structurally impossible: the runtime lives at Layer 1 (`helpers/`) and imports zero `components.*` modules. Both consumers depend on the helper; neither depends on the other. - Concurrent access is rejected at runtime by an explicit guard (`LightGlueConcurrentAccessError`), preserving the single-CUDA-stream invariant. The composition root binds the runtime to the single F3 hot-path thread; AC-4 of the contract is the canary that catches future composition-root mistakes. - The helper exposes no `set_*` / `update_*` methods — once constructed, the runtime's behaviour is fixed. ## Scope ### Included - `LightGlueRuntime(engine_handle: EngineHandle)` constructor. - `match(features_a: KeypointSet, features_b: KeypointSet) -> CorrespondenceSet` — single-pair path used by C2.5. - `match_batch(features_a_list, features_b_list) -> list[CorrespondenceSet]` — batch path used by C3. - `descriptor_dim() -> int` accessor for shape validation upstream of `match`. - Concurrent-access guard that raises `LightGlueConcurrentAccessError` on overlapping `match` / `match_batch` entries. - `LightGlueRuntimeError` (construction / dim mismatch) and `LightGlueConcurrentAccessError` (concurrent entry) exception types. - Public interface contract published at `_docs/02_document/contracts/shared_helpers/lightglue_runtime.md`. ### Excluded - Engine compilation / serialisation — C7. - Engine filename schema — `helpers.engine_filename_schema` (separate task in this epic). - Engine cache management / takeoff load — C10. - Backbone-specific feature extraction (DISK / ALIKED / XFeat) — C3 / C7. - Multi-GPU / multi-stream / mixed-backbone — out of scope for v1.0.0. - The `EngineHandle` Protocol itself — owned by `_types/manifests.py` (AZ-263) so Layer 1 can reference it without depending on C7. ## Acceptance Criteria **AC-1: Single-pair match (C2.5 path)** Given a pair of `KeypointSet`s with matching descriptor dim and a synthetic-overlap fixture When `match(features_a, features_b)` runs Then a `CorrespondenceSet` is returned with `len > 0` and the inlier-count helper used by C2.5 finds the expected count **AC-2: Batch match (C3 path)** Given three pairs of `KeypointSet`s When `match_batch([a1, a2, a3], [b1, b2, b3])` runs Then three `CorrespondenceSet`s are returned in input order; per-pair invariants match the single-pair path **AC-3: Descriptor-dim mismatch rejected** Given features whose `descriptor_dim` does not match the engine's expected dim When `match` runs Then `LightGlueRuntimeError` is raised with a message naming both the expected and actual dims **AC-4: Concurrent access rejected** Given two threads call `match` simultaneously on the same `LightGlueRuntime` instance When the second call enters Then `LightGlueConcurrentAccessError` is raised in the second thread; the first thread completes normally **AC-5: Construction-time guard** Given `LightGlueRuntime(engine_handle=None)` When construction runs Then `LightGlueRuntimeError` is raised mentioning `engine_handle` **AC-6: No upward imports — R14 structural fix** Given the helper module When a static-import check runs across `gps_denied_onboard.helpers.lightglue_runtime` Then it imports ONLY from `_types`, numpy, and stdlib — NO imports from `gps_denied_onboard.components.*` (verified by importlinter or grep gate in CI) **AC-7: Determinism downstream of the engine** Given the same `(features_a, features_b)` pair matched twice with the same `engine_handle` When `match` runs both times Then both `CorrespondenceSet` outputs are byte-equal (engine determinism is a C7 concern; this AC asserts the helper itself adds no non-determinism) ## Non-Functional Requirements **Performance** - `match` p99 ≤ 30 ms on Tier-2 with the production DISK+LightGlue engine on a typical K=10 candidate pair (matches the per-frame budget for C2.5's K=10 → N=3 funnel). - Helper-level overhead (excluding the engine call itself) ≤ 100 µs — verified via a benchmark that swaps in a stub engine handle. **Reliability** - `LightGlueRuntimeError` and `LightGlueConcurrentAccessError` are the ONLY exception types the public surface raises. Engine-internal exceptions MUST be wrapped. - Pure-deterministic given a deterministic engine; the helper itself adds no random state. **Concurrency** - Single-thread by contract. The concurrent-access guard is the runtime invariant detector — any composition-root regression that wires the runtime into multiple threads is caught immediately rather than producing GPU memory corruption. ## Unit Tests | AC Ref | What to Test | Required Outcome | |--------|-------------|-----------------| | AC-1 | single-pair match on synthetic-overlap fixture | non-empty `CorrespondenceSet` | | AC-2 | batch of 3 pairs | three results in input order; per-pair invariants match AC-1 | | AC-3 | dim-mismatched features | `LightGlueRuntimeError`; message names expected & actual dims | | AC-4 | two threads call `match` simultaneously | one succeeds; the second raises `LightGlueConcurrentAccessError` | | AC-5 | construct with `engine_handle=None` | `LightGlueRuntimeError` | | AC-6 | importlinter / grep gate over `helpers/lightglue_runtime.py` | no `components.*` imports | | AC-7 | same pair matched twice | byte-equal outputs (with deterministic stub engine) | | NFR-perf | microbench `match` overhead with stub engine (10k iterations on Tier-2 fixture) | helper overhead ≤ 100 µs | ## Constraints - Public surface frozen by `_docs/02_document/contracts/shared_helpers/lightglue_runtime.md` v1.0.0. - Layer 1 Foundation only. NO upward imports — this is the load-bearing constraint for the R14 fix. - The `EngineHandle` Protocol must be defined in `_types/manifests.py` (AZ-263 / E-BOOT) so this helper can reference it without importing C7. If `_types/manifests.py` does not yet define the Protocol surface (`forward(...)`, `descriptor_dim`), this task adds it — that is the only `_types` edit allowed by this task. - No new dependency beyond what AZ-263 / E-BOOT pinned. ## Risks & Mitigation **Risk 1: Composition root accidentally creates two runtimes (one for C2.5, one for C3)** - *Risk*: Future composition-root refactor instantiates `LightGlueRuntime` twice; engine memory doubles, behaviour drifts. - *Mitigation*: The composition-root contract test (E-CC-CONF / AZ-246, AZ-269/AZ-270 in scope) already verifies cardinality of cross-cutting helpers. This task's contract documents that EXACTLY ONE instance is expected; the composition-root validator is the enforcement point. **Risk 2: Concurrent-access guard introduces hot-path overhead** - *Risk*: A naive `threading.Lock` on every `match` call adds 100s of µs. - *Mitigation*: The guard uses a non-blocking `threading.local()`-style check or a `Lock(blocking=False).acquire()` pattern that simply RAISES on contention rather than serialising callers — the contract is "concurrent calls are a bug", not "serialise concurrent callers". NFR-perf microbench validates the overhead budget. **Risk 3: A future backbone needs a different match shape** - *Risk*: A new feature backbone produces 5-tuple correspondences instead of the current 4-tuple (e.g., adds confidence per match). - *Mitigation*: The contract version bump path is documented (`Versioning Rules` section). Adding a field is non-breaking IF consumers tolerate the extra field; otherwise it is a major-version contract change with a deprecation pass. ## Runtime Completeness - **Named capability**: shared LightGlue inference runtime with single-CUDA-stream guarantee + R14 structural cycle fix (architecture / E-CC-HELPERS / `03_helper_lightglue_runtime.md`). - **Production code that must exist**: real `EngineHandle`-backed match dispatch; real concurrent-access guard; real descriptor-dim validation. - **Allowed external stubs**: a deterministic stub `EngineHandle` is allowed in tests (and recommended for AC-7 determinism) but production paths use C7's real engine. - **Unacceptable substitutes**: bypassing the concurrent-access guard with `threading.Lock` (silently serialising callers); allowing each consumer to construct its own runtime; reintroducing a C2.5 → C3 (or C3 → C2.5) import to "share state". Any of those reintroduces R14. ## Contract This task produces the contract at `_docs/02_document/contracts/shared_helpers/lightglue_runtime.md`. Consumers MUST read that file — not this task spec — to discover the interface.