6.7 KiB
ML inference cache & thundering-herd control
PoC design for reusing segmentation / angle / inflammation results across page refresh, multiple browser tabs, and duplicate UI triggers — without re-calling Modal Triton for the same profile.
Problem
| Trigger | Before cache |
|---|---|
| Open clinical workspace (3 frames) | 2 proxy HTTP + ~9 Triton calls |
| Refresh page | Full repeat (in-memory cache lost) |
| Second tab, same patient | Full repeat (separate JS heap) |
| React Strict Mode double-mount | Partially deduped in one tab only |
| Identical batch POST while first in flight | Proxy runs work again (queue only) |
Cache identity (invariants)
A cached result is valid when all of these match:
patientMrn — e.g. BN-2024-1847 (encounter / patient scope)
frameId — e.g. med-lat-1
contentHash — SHA-256 of frame image bytes (invalidates on re-scan)
pipelineVersion — model + postprocess fingerprint (invalidates on deploy)
Cache key string:
{patientMrn}|{frameId}|{contentHash}|{pipelineVersion}
Batch coordination key (cross-tab single-flight):
{patientMrn}|{pipelineVersion}|seg:{angleType}:{sortedFrameIds}
{patientMrn}|{pipelineVersion}|angle:{sortedFrameIds}
CV pipeline (spec §7)
Frontend calls POST /api/test/analyze/batch — one orchestrated path per frame:
CLAHE → angle classification → (post-trans | sup-up-long only) inflammation
→ if inflammation: segmentation + thickness + severity + overlay
→ else: severity level 0, segmentation skipped
→ other angles: angle + severity 0 only
Pipeline version: poc-v2-spec-cv (ML_INFERENCE_PIPELINE_VERSION / PROXY_PIPELINE_VERSION).
Legacy split endpoints (/segment/batch, /angle/batch) remain for debugging but bypass spec gating.
Two layers
Layer 1 — IndexedDB (persist + refresh)
- Database:
lumina-msk-ml/ storeframe-inference-cache - Survives refresh and is shared across tabs on the same origin
- TTL: 24 hours (
cachedAt); stale entries ignored - Stores per frame:
- segmentation:
overlaySrc,rawbackend payload subset - angle:
AngleClassificationResult - inflammation is embedded in segmentation
raw.inflammation
- segmentation:
Layer 2 — BroadcastChannel (thundering herd)
- Channel:
lumina-ml-inflight - Messages:
fetch-start/fetch-done/fetch-errorwithbatchKey - Tab that starts a network batch announces
fetch-start - Other tabs await
fetch-donefor the samebatchKey, then read IndexedDB - Fixes race where two cold tabs both miss IDB before either writes
In-tab inflightBatchByKey Map remains as a third line of defense.
Frontend call flow
getSegmentationResultsForProfile(frames, { patientMrn })
1. For each frame: read IDB by cache key → hydrate memory cache
2. If all frames hit → return (0 HTTP)
3. batchKey = profile batch key
4. await crossTab.waitIfInflight(batchKey) // another tab may have finished
5. Re-check IDB + memory
6. in-flight Map single-flight → fetchSegmentationBatchForProfile
7. Write IDB per frame; crossTab.notifyDone(batchKey)
Angle batch follows the same pattern via getAngleClassificationResultsForProfile.
useSegmentationOverlay passes patientMrn from ClinicalWorkspacePage → DiagnosticCanvas.
Proxy server cache (test_fast_api_proxy.py)
In-memory (process lifetime):
| Map | Purpose |
|---|---|
_proxy_result_cache |
cache_key → (expires_monotonic, json-serializable result) |
_proxy_inflight |
cache_key → asyncio.Future — coalesce identical batch requests |
Segment batch key:
segment|{angle_type}|{pipeline_version}|{sorted(frame_id:sha256(image_bytes))}
Angle batch key:
angle|{pipeline_version}|{sorted(frame_id:sha256(image_bytes))}
TTL default: 1 hour (PROXY_RESULT_CACHE_TTL_S env).
On cache hit: return cached JSON immediately (0 Triton). On concurrent miss: later request awaits the first request's Future.
Pipeline version
Frontend: ML_INFERENCE_PIPELINE_VERSION in mlInferencePipelineVersion.ts
Proxy: PROXY_PIPELINE_VERSION env (default poc-v1)
Bump both when Triton model names or overlay/postprocess logic change.
Debugging
| Symptom | Check |
|---|---|
| Still 9 Triton calls every refresh | IDB miss — DevTools → Application → IndexedDB → lumina-msk-ml |
| Two tabs both fetch | BroadcastChannel not supported or different patientMrn / pipelineVersion |
| Stale overlay after model update | Bump ML_INFERENCE_PIPELINE_VERSION |
| Proxy logs "cache hit" | Working — no Triton for that batch |
| 502 herd | Separate issue — lock + retries; cache reduces frequency |
Files
| File | Role |
|---|---|
docs/ml-inference-cache.md |
This document |
frontend/.../mlInferencePipelineVersion.ts |
Version constant |
frontend/.../mlInferenceCacheStore.ts |
IndexedDB read/write |
frontend/.../mlInferenceCrossTab.ts |
BroadcastChannel coordinator |
frontend/.../cvAnalyzeApi.ts |
Spec CV pipeline — single /api/test/analyze/batch call |
frontend/.../angleClassificationApi.ts |
IDB + cross-tab integration |
backend/tests/test_fast_api_proxy.py |
Server result cache + in-flight dedupe |
frontend/.../mlServiceError.ts |
Classify raw errors → ticket-friendly MlServiceError |
frontend/.../MlServiceErrorPanel.tsx |
Canvas overlay UI (support ref, copy, retry) |
ML error UX (ticket-friendly)
When segmentation fails, the canvas shows MlServiceErrorPanel instead of raw browser messages (e.g. Safari’s “The string did not match the expected pattern”).
| Audience | What they see |
|---|---|
| Clinician | Reassuring title + plain Vietnamese explanation; ultrasound image unchanged |
| IT / ticket | Mã tham chiếu (LUM-ML-…) + Sao chép cho ticket button |
| Developer | Collapsed Chi tiết kỹ thuật — operation, frame id, raw message, remediation hint |
Error codes: BAD_RESPONSE (proxy down / HTML not JSON), NETWORK, SERVER_OVERLOAD (502/503/Triton), SERVER_UNAVAILABLE, CLIENT_ERROR, NO_RESULT, UNKNOWN.
Debugging: Ask user for support reference → match browser console Network tab (/api/test/segment/batch) with proxy logs. Common root cause for BAD_RESPONSE: test_fast_api_proxy.py not running on port 8001.
Worklist AI grade
Home-screen Đề xuất AI never uses MOCK_PATIENTS.synovitisGrade.
| State | Card shows |
|---|---|
| No cached inference | Chưa phân tích |
| Reading IndexedDB | Đang tải… |
| After workspace ML run | Độ X — max severity.level across cached profile frames for that patientMrn |
Grades refresh when the worklist tab becomes visible again (return from clinical workspace).