R8 legacy retirement — measured scope (2026-09-28)¶
Scope notes for the retirement slices, written on the GB300 dev partition [V: this sandbox is the agent partition per ledger E10]. Nothing here has been executed — no file has been deleted, no test tier run. Every line reference was read at the tip of feat/vss-gaming-gpu-profile (1f2921a4) and re-checked by hand; the parts a 7-agent scoping pass asserted and I could not reproduce are marked as such. It is the detail layer for the goal prompt at docs/plans/2026-09-29-r8-legacy-retirement-goal-prompt.md; that prompt is the order of work, this file is the evidence.
Owner rulings that set the scope (both 2026-09-29, recorded here before the work):
- "we do not have to support backwards compatability" and "we should hard raise" — so
PIPELINE_MODE=legacymust RAISE at boot, not warn. This matches ledger item 20's near-identical wording on the re-ID swap ("full swap. ignore previous architecture…"). - The R8 trigger is reversed by instruction:
12-postponed-roadmap.md:23defers this deletion to "Go-live holds 14 days"; the owner is picking it up now, per that file's own convention at:10(a picked-up item gets its own dated spec and a mark here). - No A5500. Everything below is repo + GB300 work. The pinned PASS numbers (S1 9,722 MiB of 20,890; S4 p95 16.2 s of 30 s, ledger items 40–41) belong to that box and must not be re-measured or re-stated as repo claims.
1. What is actually reachable today [V]¶
The legacy path is live-by-environment-variable, so its removal is a functional change, not a cosmetic one.
backend/core/config.py:1061-1090— the validator's own comment says"legacy" parses only because its code stays in the repo until R8 deletes it.:1084accepts the two spellings,:1086logs a warning. It does not reject.- Consequence: a deployment with a stale
PIPELINE_MODE=legacyin its.envboots today and runs the unsupported pipeline. Under the hard-raise ruling it must crash at boot.
1a. Two shared config pairs that are NOT legacy-only [V]¶
GPU_LLMis the LLM's and the VLM's GPU:docker-compose.prod.ymlpasses it toai-llm(:145,:159,:209) and toai-vlm(:251,:266,:35x). Deleting the ai-llm block must not delete the variable or its.env.examplerow.CTX_SIZE/PARALLELis one env pair feeding two different slot budgets:config.pybindsnemotron_context_window→CTX_SIZE(legacy, 262144) andllama_slot_count→PARALLEL, while the shipped vlm slot uses 32768/2. Removing the legacy defaults must leave the vlm binding untouched — this pair is exactly where a careless deletion changes the measured slot ceiling behind S1's back.nemotron_urlhas live, non-mode-gated consumers beyond the legacy pipeline:summary_generator.py:84,prompt_service.py:679,pipeline_quality_audit_service.py:136,performance_collector.py:475. Whether those features are live under vlm mode is not established here — §11 item 5.
2. The legacy branch sites [V]¶
Instrument (wide grep — the narrow one is a trap, see below):
grep -rn '"legacy"' backend/ --include=*.py | grep -v tests # 13 lines at the tip
Trap: the narrower grep -rn 'pipeline_mode == "legacy"\|pipeline_mode != "legacy"' returns 7 and silently misses backend/services/pipeline_factory.py:52, which branches on a local mode variable assigned at :51 (mode = _settings_for_mode().pipeline_mode). An agent that enumerates with the narrow grep ships a live legacy branch.
Sites, all [V] at the tip: main.py:733, main.py:1134; pipeline_factory.py:52 (the module is 59 lines and is essentially the mode switch itself); system_broadcaster.py:1028; container_orchestrator.py:536; core/container.py:508; api/routes/health_ai_services.py:111; api/routes/system.py:1164 (+ the legacy health body it short-circuits); api/schemas/enrichment_data.py:442,454; config.py:1084,1086.
pipeline_workers.py:800 is only a comment — its real coupling is the from backend.services.nemotron_analyzer import NemotronAnalyzer at :67.
3. nemotron_analyzer.py is NOT wholly legacy [V — this was the pass's most important catch]¶
backend/services/nemotron_analyzer.py is 5,429 lines; ~98% is legacy. But :294-411 is imported by the VLM path, verified at each importer:
| symbol | imported by |
|---|---|
ConstrainedDecodingNotEnforced | vlm_client.py:65-69, vlm_analyzer.py:74, evaluation/vlm_replay.py:56, main.py:773 |
_is_length_truncated | vlm_client.py:65-69 |
build_probe_schema | vlm_client.py:65-69 |
main.py:773 is inside the live constrained-decoding startup gate — the comment at main.py:769 says the VLM mode's gate "shares the same shared ConstrainedDecodingNotEnforced verdict vocabulary". Deleting the file whole therefore breaks the shipped pipeline at import, and the S-1 enforcement verdict with it.
Slice order: 2a hoist :294-411 (also carries VerificationRowOutcome:294, PROBE_PROMPT:323, _TRUNCATED_STOPS:335, _is_length_truncated:338, _probe_completion:352, build_probe_schema:396) into backend/services/constrained_decoding.py, repoint the four importers, red-first with an import test. 2b deletes the ~5,321 remaining lines.
Adjacent legacy-only modules — sizes now wc-verified [V]: enrichment_pipeline.py 7,662 (the pass's "3,334" was enrichment_client.py's number, swapped), enrichment_client.py 3,334, nemotron_streaming.py 575, analyzer_facade.py 266, prompt_auto_tuner.py 205, nemotron_latency_optimizer.py 650, plus two the first pass missed: skeleton_action_service.py (only importer: enrichment_pipeline) and pose_analysis_service.py (zero non-test importers — already orphaned). DI/type bindings naming NemotronAnalyzer also live at ai_fallback.py:47,238 (+health use :433), api/routes/events.py:100,105,2584, pipeline_workers.py:67, api/dependencies.py:346,949,1430, core/dependencies.py:60,100 — several are TYPE_CHECKING-only imports (from backend.services... import under if TYPE_CHECKING:), which erase at runtime: they must be edited, but deleting the module does not crash them at import — distinguish those from the eager ones (events.py:100 is a real runtime import).
Safe to delete without touching M1: nemotron_analyzer.py has zero hits for should_notify|NotificationFilter|notification_filter [V], so the unwired M1 rule at notification_filter.py:35 survives the deletion untouched.
4. Enrichment and the specialists do not overlap [V]¶
backend/services/vlm_specialists.py has zero references to enrichment — the face, plate and re-ID legs are built from the module's own handles (collect_face_text:499, collect_plate_text:552, collect_reid_text:645). Deleting enrichment_client.py (3,334 L) and enrichment_pipeline.py cannot orphan the specialists.
But the package init is the sever-most cut [V]. backend/services/__init__.py eagerly re-exports both targets with no try/except: EnrichmentPipeline et al. at :95 and NemotronAnalyzer at :245. Deleting either module without first editing that file breaks import backend.services everywhere at once — the whole tier, not one caller. Edit the init in the same slice, ahead of the rm.
Other shared infra that is not legacy and must survive [V]:
llama-cachenamed volume (docker-compose.prod.yml:1621) is mounted by ai-vlm at:262(and ai-llm at:152). Deleting the ai-llm service block must not delete the volume — that takes the VLM's CUDA/JIT caches with it.- The three specialist loaders stay:
face_recognizer_loader.py,osnet_loader.py, and the plate leg.osnet_loaderreads its weights and pin from the models.yml row that also carriestriton_name: reid— one row feeds the CPU in-process loader and the Triton repository. Keep the whole row; the row is entangled, not just the Triton half. - Florence is legacy-reachable, not VLM-reachable [V] (this corrects an earlier, too-strong draft line that put
prompt_service.pyon the live list — its only Florence token is the docstring example at:712). The real Florence runtime importers areservices/__init__.py:136,145(the eager re-export — the sever-most cut, above) andmodel_zoo.py:66(the_LOADER_MAPwiring).florence-2-largeis alreadyenabled: false;florence-2-baseis the Triton row. Trace the wiring, don't trust R8's roadmap line — see the loader census below. ai/gateway/main.py:60,66buildsALL_MODELSfromlist(FULL_MODEL_SET); pruning FULL_MODEL_SET must move that and the/healthreporting together, or the gateway advertises models its repository no longer serves.api/routes/model_management.py:162ROUTER_SUFFIXES = ("/enrichment", "/enrich-lt")— the live model-readiness API is built on the two enrichment routers, so the frontend readiness surface breaks the moment those routers go. Re-scope the endpoint, don't just delete.models.ymlis one file, many readers (setup_lib/model_downloader,services/model_zoo, gateway residency, the precheck) on three different filters. A row's removal has four blast radii; treat the file as the shared surface it is.
4a. The keeps/goes census: the enrichment tier is legacy-only by construction [V]¶
Answering the owner's "we keep the VLM and the supplemental models, right?" — yes, but the reason is stronger than a list of keepers, and it is what makes R8's :109 phrase "the in-process loaders the VLM path no longer calls" true rather than hopeful.
The construction gate. core/container.py:508 builds the analyzer's enrichment collaborators only inside if get_settings().pipeline_mode == "legacy": — context_enricher and enrichment_pipeline are passed as kwargs then and never otherwise (vlm_analyzer does not take them). So once legacy is gone, EnrichmentPipeline has no construction site and no API route reaches it (grep enrichment_pipeline backend/api/routes/ = 0 hits [V]). The whole enrichment tier is not "decided to be legacy" — it becomes unreachable the moment S1 lands. R8's "What stays: the face, re-ID and plate specialists" (:114) is therefore an accurate, not merely aspirational, line.
The loader census — all 22 backend/services/*_loader.py, classified by real (non- docstring) importers, marking VLM-path reachability [V]:
| verdict | loaders |
|---|---|
| KEEP — VLM-reachable (3) | face_recognizer_loader (vlm_specialists.py:174,328,345,535), osnet_loader (vlm_specialists.py:705, reid_service.py), fast_alpr_loader (vlm_specialists.py:566 — the plate leg) |
| legacy-only (19) | age_classifier, clip, depth_anything, fashion_clip, florence, gender_classifier, image_quality, pet_classifier, segformer, smoke_fire, stgcn, threat_detection, vehicle_classifier, vehicle_damage, violence, weather, yolo_world, zero_dce, vitpose — every one reachable only via enrichment_pipeline / model_zoo._LOADER_MAP / legacy prompts.py |
| shared base (1) | model_loader_base.py — KEEP, it is the base class of the three survivors |
Two traps inside that census:
prompts.pyis not evidence of life. Its loader imports at:674-691sit underif TYPE_CHECKING:— type-only, erased at runtime. An import grep that doesn't exclude theTYPE_CHECKINGblock reads 15 legacy loaders as live consumers.prompts.py's real importers (nemotron_analyzer,nemotron_streaming,analyzer_facade,enrichment_pipeline) are all legacy-side.threat_detection_loaderis legacy but the Tritonthreatmodel is not. Two different implementations, same word. Keepresidency.py:69VLM_THREAT_MODELand themodels.yml:309 triton_name: threatrow;threat_detection_loadergoes with the tier.
Preload residency, so the S1 number is understood [V]: BACKEND_MODEL_PRELOAD (config.py:2886, default false) makes main.py:644 select_preload_candidates load every enabled row that declares preload: true — exactly four: osnet-ain-x1-0 (100 MB, F11), face-detector-scrfd (0), face-recognizer (0), and smoke-fire-yolov8n (350 MB, enrichment-only — imported only by enrichment_pipeline/model_zoo/nemotron_analyzer). So the shipped VLM path preloads three GPU legs, and the fourth preloaded model is dead in vlm mode. Its row's comment even says "CRITICAL: never evict, preload at startup", which select_preload_candidates' own docstring records as "decorative" — never_evict has no consumer. Retiring smoke-fire means removing that row's preload: true too, or a future agent re-reads it as resident.
5. Triton residency — there is no S1 risk here [V]¶
This section exists because an earlier draft of this file asked the owner an ill-posed question about "moving a Triton row". The facts dissolve it:
ai/gateway/residency.py:65—VLM_MODEL_BASE = ("yolo26", "reid");:69addsthreatas an opt-in. Shipped state isGATEWAY_ENABLE_THREAT=false(docker-compose.prod.yml:482), so the shipped set is exactly yolo26 + reid, and that is the footprint S1's 9,722 MiB was measured against.FULL_MODEL_SETat:46-60holds 14 models; 11 are legacy-only —clip, clip_text, florence2, vehicle, fashion_clip, demographics_age, demographics_gender, pet, depth, pose, stgcn_action. Pruning those cannot move S1's number.threatis not legacy — it is the vlm set's optional fourth. Keep it.- Face is not in Triton at all. SCRFD + w600k_r50 run CPU-side in the backend, loaded by
face_recognizer_loader(vlm_specialists.py:328). Do not look for them in the gateway. - Tests that pin the pair and must move with the change:
backend/tests/unit/core/test_gateway_model_set_compose.py,ai/gateway/tests/test_residency.py,ai/gateway/tests/test_entrypoint_residency.py.
Open, low-stakes: once legacy is gone, get_model_set("full") serves only a deliberate "bring the whole repo up" dev run (residency.py:26). Default is to delete it and make GATEWAY_MODEL_SET hard-raise on anything but vlm, consistent with the no-backcompat ruling; keeping it as a dev affordance is the owner's call.
6. Tables — no Alembic, and two tables that look alike but are not [V]¶
There is no Alembic in this repo: schema comes from backend/scripts/init_schema.py + core/database.py, with hand-authored dated SQL in docs/api/migrations/ (precedents: 2026-09-26-face-vector-provenance-model-id.sql, NEM-5051-convert-alert-json-to-jsonb.sql). So retiring a table = delete the model class + add a dated DROP SQL file.
backend/models/enrichment.pyis not a wholesale delete.pose_results:47,demographics_results:160,action_results:265,reid_embeddings:218are legacy-only —reid_embeddingsis written only byenrichment_pipeline.py:5848.threat_detections:100stays — read byservices/alert_engine.pyandapi/schemas/alerts.py.- The name trap:
enrichment.reid_embeddingsis legacy, butbackend/models/household.py:155PersonEmbeddingis LIVE — the VLM re-ID leg loads its gallery throughhousehold_matcher.load_person_gallery:211, which is deliberately the single reader so the one-embedding-space rule (F11/item 20) holds exactly once. Also live:face_identity.py:80 known_persons,:139 face_embeddings.
7. ai_contract is the real gate on deleting ai/ [V]¶
The retired serving dirs (ai/florence, ai/enrichment, ai/enrichment-light, ai/clip, ai/triton) are referenced from backend/ai_contract/ (operations.py, providers.py, fake/generators.py) and backend/tests/contracts/ai_providers/*. That contract tier is PR-gated (Contract Tests (API Schema) runs uv run pytest backend/tests/contracts/ -n0 --timeout=30, .github/workflows/ci.yml:1363).
Named first: operations.py:380-391 declares an llm_completion Operation whose evidence string cites nemotron_analyzer.py:465, with schemas/llm_completion.{request,response}. That row must retire in the same slice that deletes the analyzer, or the tier goes red. Conclusion: retire one provider per slice, never rm -rf ai/<dir>.
8. Test and CI blast radius [A where not marked V]¶
- Coverage gates are the merged floors, not pyproject.
ci.yml:613fails under 84 (unit),ci.yml:1215under 37 (integration) [V].pyproject.toml:636 fail_under = 85is the local gate; the unit shards run--cov-fail-under=0and only collect (ci.yml:430-467) [V]. Deleting legacy code moves the denominator in a direction nobody has measured — measure per slice, report the sign, never lower a floor. - Legacy-only test mass: ~96 files / 86,970 lines / 3,175 AST test defs (89 unit files / 3,050 defs). ~61 further files merely mention legacy and must be edited, not deleted (example given by the pass:
test_model_warmup.py, 41 nemotron refs, subject is warmup). - The suppression census, not a "test-count mirror." The
99→101in PR 6698's own commit messages is categorypytest_skip_imperativeof the escape-hatch census, adjudicated byea79a858and gated atci.yml:101-104against.github/suppression-baseline.json, regenerated byscripts/suppression-registry-gen.py[V]. Deleting test files changes that count → regenerate, never hand-edit. (An earlier draft called this a test-count mirror; that was wrong and the pass corrected it.) - Environmental noise: 66 of those 96 files error at import in this sandbox for want of
libxcb.so.1(cv2 is absent — reproduced). Judge deletions by CI, not local runs. - No test anywhere asserts a deleted module is absent [A — the pass grepped for it and found only the stale-pycache scan at
scripts/a5500_precheck.py:36]. So the grep-gate in S1 is a test the implementer must author.
9. Docs a future agent reads [V for path existence]¶
- Currency, ship alone: dated in-doc banners over
docs/vss-integration/README.md:6andAGENTS.md:6, both claiming "nothing implemented yet" — false since4bfd6fa4made vlm the shipped default (docker-compose.prod.yml:578). Precedent to imitate:06-repo-a-readiness.md:11"Retirement note — 2026-09-21 [V]". Also00-context.md:5("Research in progress"),README.md:18-19(credits the Brev matrix with measurements that never ran, ledger:148), and ledger:3's rev-5 cite (spec is rev 7). Never rewrite frozen research prose, and never edit a spec's Status/rev line — that is owner-only house convention, not drift. - Post-deletion: root
README.md/AGENTS.md,ai/AGENTS.md,backend/AGENTS.md,mkdocs.ymlnav,docs/developer/{moe-offloading,llm-inference-optimization,multi-gpu}.md,docs/reference/{models,nvidia-technology-inventory}.md,llms.txt. (Two paths an earlier draft asserted do not exist:docs/developer/nvidia-technology-inventory.mdis actuallydocs/reference/…, and there is noai/llm/— the build context is./ai/nemotronatprod.yml:139.) docker-compose.ghcr.ymlhas noai-vlmservice at all — it cannot serve the shipped mode today. Report it; do not "fix" it inside a retirement slice.docs/superpowers/plans/2026-09-27-a5500-vlm-bringup-checklist.md:42-44is GENERATED and already stale. If a slice breaks a generator assertion, report it — never hand-edit a render (scripts/a5500_precheck.pyis the authority).
10. Known-truth hazards carried forward¶
- Ledger items 39–41 cite commit hashes (
2fd5c54bc,d7b3fd3c6,cedac0a64, …) that do not resolve as git objects at this tip — a branch rewrite, not fabrication. A hash grep reads the newest evidence as invented; it isn't. - The newest S4 number is 16.2 s (item 41), superseding item 39's 21.7 s.
originin this sandbox is the dead host path/home/msvoboda/github/…; the real branch is on remotegithub, andghneeds--repo mikesvoboda/nemotron-v3-home-security-intelligence.- Do not delete
notification_filter.py:35 should_notify. All three verdict rules are implemented; only the wiring is missing. That is M1's exit, a separate job.
11. Not decided here (owner's call, with the recommended default)¶
models.yml: delete legacy LLM rows, or keepenabled: falseas provenance? The X-CLIP precedent kept them and calls the file "owner-owned, untouched". Default: ask before deleting.- Does R8 reach the enrichment AI providers (
ai/enrichment,ai/enrichment-light) or only the backend client + pipeline + legacy LLM? R8's own text at:108-115reads wider than the ledger's description. Default: backend first, providers as their own slice. - Keep
fullas a Triton dev affordance? Default: no — hard-raiseGATEWAY_MODEL_SET. - Whether R8's "(including the 0.3 changes)" imposes anything beyond deleting the file.
nemotron_urlis read bysummary_generator.py:84,prompt_service.py:679,pipeline_quality_audit_service.py:136andperformance_collector.py:475— none of them mode-gated. Two of the four are confirmed live callers, not just readers [V]:summary_generator.py:84(the hourly dashboard summary — its scheduler start atmain.py:1092is gated only onredis_client is not None, NOT on mode) andprompt_servicewhichPOSTs{_llm_url}/completionat:938(registered router:main.py:1649). Becauseai-llmisprofiles: [legacy](prod.yml:125), it is already down in the shipped vlm mode, so both features degrade to warnings today (summary_job.py:252-255catchesTimeoutError) — deleting the LLM makes a currently-incident breakage permanent. Owner: is a vlm-backed summarizer wanted (point it atai_vlm_url), or is the feature retired with the LLM? Decide before S2; do not silently orphan it.- ~~Florence's live non-mode-gated consumers~~ — RESOLVED [V], see §4's census. Florence is legacy-reachable, not VLM-reachable; the only runtime importers are the eager
services/__init__.py:136,145re-export (a sever point, not evidence of life) andmodel_zoo.py:66.prompt_service's only Florence token is a docstring example (:712). Default: delete with the tier, after editing the__init__re-export. - Does retiring the attribute zoo cost understanding the owner wants back? (owner asked 2026-09-29; recommendation here, decision is the owner's.) The VLM receives the raw key frames as base64 (
vlm_client.py:499), so Florence-2 and the 19 attribute loaders re-perceive what the 8B already sees — re-adding them (even "with a more modern version") is a weaker parallel perception, not new information. The three survivors stay for a different reason: they are lookups against a store the pixels can't reach (face→knownpersons, plate→registered vehicles, re-ID→PersonEmbeddinggallery viaload_person_gallery). If the owner wants more signal, the category worth adding is _more detection/lookup, not more perception: (a) switch on the already-sanctioned threat specialist —GATEWAY_ENABLE_THREAT(prod.yml:482, default false), resident atresidency.py:69, D3 rev-6's optional fourth; it is a precision/recall detector (not a describer) and the one legitimately worth modernizing offthreat-detection-yolov8n(models.yml:309). Costs S1 VRAM — re-measure if enabled. (b) R11 glass-break audio is the other off-pixels signal, deferred (no consumer card fits the audio stack). Counter-evidence against re-bolting the zoo: S3 already fails 90% in every arm for scoring reasons, and moves ~3/20 between identical runs (ledger:403) — attribute text adds reconciliation surface below the noise floor, inside a 1024-token verdict budget (vlm_client.py:91). Default: proceed with the R8 deletion; treat the threat detector as a separate opt-in decision.
12. Reconciliation of the 14-agent scoping workflow (wf_5ccaa8ff-0c7) [V]¶
Every line below was re-read at the tip after the workflow returned. The workflow's claims were largely sound; a meaningful number of its paths and line numbers were not, so nothing was folded in without a re-read.
Refuter claims I REJECTED — the refuter was wrong, my original [V] stands:
model_management.py:162 ROUTER_SUFFIXES = ("/enrichment", "/enrich-lt")exists (re-read verbatim; the two lanes are explained at:106/:121). §4 keeps it.main.py:733 if settings.pipeline_mode == "legacy":exists (re-read verbatim).ai_fallback.pyis line drift, not fabrication: theNemotronAnalyzerbinding is:47and:238; the health-check use is:433(the spine cites:434). Off by one, real symbol.
Where BOTH sides had the wrong path (the signal is real; the citation was not):
- The frontend is
frontend/, notsrc/frontend/. Every workflow cite of the formsrc/frontend/src/components/enrichment-panel.ts:19-31is a hallucinated path; my own first grep ofsrc/frontendcame back empty for the same reason. On the real tree the enrichment surface is 106 files, and the child-object renderers R8's:108("their frontend panels") actually names are real:components/enrichment/EnrichmentViewer.tsx,components/events/EnrichmentBadges.tsx,EnrichmentPanel.tsx,EventEnrichmentSummary.tsx, plusutils/poseVisualization.ts(draws COCO-17 skeletons from the retired pose keypoints). - Four enrichment config fields exist and are dead in vlm mode, but at the real lines
florence_url:1582, clip_url:1586, enrichment_light_url:1594(+enrichment_url, and the:1831validator list) — not the:751-818range the pass reported.
NEW, confirmed, must be in scope (missed by my first pass):
backend/services/skeleton_action_service.py— imported only byenrichment_pipeline.py, zero VLM-path importers → legacy-side, retires with the tier.backend/services/pose_analysis_service.py— zero non-test importers at all (already orphaned) → retires with the tier.
Also corrected: enrichment_pipeline.py is 7,662 lines, not the 3,334 the scoping pass reported (that is enrichment_client.py's size — the two were swapped).
A sequencing argument worth adopting — do the table/row retirements FIRST [V reasoning]. select_preload_candidates (main.py:644) preloads by enabled AND preload from models.yml; nothing in it is keyed to pipeline_mode, so smoke-fire's 350 MB preload runs in shipped vlm mode today. S1's 9,722 MiB PASS was measured with that dead 350 MB inside the budget, so retiring smoke-fire moves S1 down — freeing headroom, never risking the bar (an owner-asked question this answers). The workflow's strongest single finding: it argues the table/row retirements belong ahead of the code deletions, not after — retiring a table's model class + dated DROP is a smaller, more reversible cut than deleting a module the DI graph still names. The spine keeps its one-slice-one-PR rule either way; this only reorders which slice goes first.