lardon3d/docs/architecture/matcher.md
fy59 599be97fbe feat(governor): freeze adaptive GPU-first orchestration
Freeze Compute Governor v2 and asynchronous Vulkan Matcher execution.

Governor now owns production task admission and live resource adaptation,
with GPU-first AUTO selection for validated backends, CPU12 host capacity,
desktop CPU/RAM reserves, UMA accounting, pressure throttling, hysteresis,
and recovery.

Validate and freeze the rolling Vulkan ORB Matcher path, host topology
policy, task capability envelopes, runtime telemetry, restart/durability
contracts, portable CPU fallback, and scientific equivalence.

COMPUTE_GOVERNOR_V2=PASS/FROZEN
ORB_VULKAN_ASYNC_EXECUTION=PASS/FROZEN
MATCHER_GPU=EXISTING_BACKEND_VALIDATED_AND_PREFERRED
2026-08-30 22:17:51 +02:00

23 KiB
Raw Blame History

Matcher v1 et Match Store v1

Statut opérationnel

INTERNAL_PARALLELISM_COMPUTE_RESOURCES_V1 — PASS / FROZEN. COMPUTE_GOVERNOR_V2 — PASS / FROZEN. ORB_VULKAN_ASYNC_EXECUTION — PASS / FROZEN.

Contrat

Le pipeline v1 est ORB → BFMatcher Hamming ou SIFT/RootSIFT → BFMatcher L2, puis KNN k=2, Lowe ratio, tri canonique, Match File content-addressed et Match Result. Il s'arrête avant toute vérification géométrique.

Pour chaque descripteur A, OpenCV fournit zéro, un ou deux voisins. Zéro ne produit rien. Un seul voisin valide est accepté. Avec deux voisins, le premier est accepté exactement si d1 < threshold * d2; une seconde distance nulle, une égalité, NaN, Inf ou une distance négative est rejetée. -0.0 est accepté comme zéro conformément à IEEE-754. Indices invalides et distances du premier voisin non finies ou négatives sont rejetés.

Un feature_index_a produit donc au maximum une correspondance acceptée. Sans cross-check, plusieurs indices A peuvent viser le même index B. L'ordre final est (feature_index_a, distance, feature_index_b) croissant. BFMatcher fournit une liste KNN par query et le filtre ne conserve que son premier voisin : la déduplication est donc mathématiquement inutile. Le Matcher rejette toute sortie anormale contenant deux fois le même index A.

Match File v1

Header fixe de 32 octets, tous les entiers en little-endian :

0..3    octets ASCII exacts "L3DM"
4       format_version = 1
5       descriptor_type (1=U8, 2=F32)
6..7    reserved = 0
8..11   match_count uint32
12..15  descriptor_dimension uint32 (32 ou 128)
16..23  feature_set_id_a uint64
24..31  feature_set_id_b uint64

Chaque entrée fait 12 octets : index A uint32, index B uint32, distance float32. Le format accepte zéro entrée pour tester reader/writer, mais le Matcher ne publie pas d'asset vide. La borne est 8192 entrées et la taille maximale exacte est 32 + 8192 * 12 = 98336 octets, environ 96 Kio.

Le reader valide avant toute allocation : magic physique, version, reserved, type/dimension, borne du compte, calcul de taille, taille physique exacte, Feature Set IDs A/B sans permutation, indices contre les comptes attendus et distance finie non négative. Fichiers tronqués, trailing bytes et versions futures sont rejetés.

Publication et reuse

Pour NO_MATCH, le fichier temporaire est supprimé et seule une ligne sans asset est créée. Pour MATCHED : temp dans assets/matches, écriture complète, fsync du fichier, SHA-256, chemin assets/matches/<préfixe>/<sha256>, puis publication atomique par link. Une race de même contenu est validée; un contenu différent déjà présent n'est jamais écrasé lors d'une publication fraîche. Lorsqu'une ligne existante prouve que son asset content-addressed est corrompu, le Matcher recalcule puis remplace atomiquement cet asset par rename et répare les métadonnées de la même ligne sous transaction. Le temp est nettoyé et le répertoire final synchronisé.

Le reuse MATCHED exige métadonnées complètes, fichier régulier non symbolique, taille, SHA-256, header, type/dimension, IDs A/B, compte et entrées valides. Il n'existe aucun fallback faisant confiance au chemin ou à l'existence seuls. La validation de reuse lit le fichier borné une seule fois et calcule le SHA-256 sur ce même buffer.

Bornes mémoire et performance

Les deux buffers de descripteurs contigus occupent au maximum 512 Kio pour ORB ou 8 Mio pour SIFT/RootSIFT. Les résultats KNN ne contiennent que deux DMatch par query, les matches filtrés et le Match File sont chacun bornés à environ 96 Kio. Le working set contrôlé du Matcher est donc inférieur à environ 10 Mio, hors scratch interne borné par OpenCV; aucune structure A × B n'est matérialisée.

Le Match File complet est sérialisé dans un buffer heap borné à 98336 octets et écrit par un unique write_exact, puis synchronisé une fois. Les mesures locales restent dans le rapport de session, pas dans ce contrat canonique. À 8192 features, le coût CPU dominant reste l'évaluation exacte des distances dans cv::BFMatcher::knnMatch. ORB peut remplacer ce seul hot path par Vulkan. Une feasibility réelle a rejeté Vulkan pour SIFT et RootSIFT : leur accumulation flottante ne garantit pas le top-2 OpenCV sur les égalités adversariales et le gain n'est présent que sur les grandes paires carrées. Ils restent intégralement sur BFMatcher CPU.

INTERNAL_PARALLELISM_COMPUTE_RESOURCES_V1 — PASS / FROZEN. matcher.run v1 orchestre le Matcher sans connaître son backend interne. La tâche persiste uniquement la configuration, l'identité des Feature Sets à sélectionner et un curseur Candidate Pair. Une paire est atomique et publiée immédiatement. Les lots bornés sont séparés par checkpoint et task_sequence_break(). La tâche utilise l'unique Resource Governor avec une estimation couvrant ce working set ; aucune seconde logique de budget n'est introduite. Dans un lot, des fenêtres bornées calculent au plus deux paires par thread CPU effectivement admis, avec un plafond opérationnel validé de douze. Le callback Queue compte comme un participant, joint au plus cpu_threads - 1 enfants, puis publie seul les stages en ordre candidate_pair_id. OpenCV reste à un thread interne pendant cette séquence, ce qui interdit un fan-out BFMatcher imbriqué. Le curseur ne suit que le préfixe contigu durable ; le premier échec scelle tout le suffixe calculé. Sa ligne durable matcher_tasks appartient au schéma Project DB v11 ; le Match Result reste le contrat publié en v10.

Compute Governor v2 fait évoluer seulement le choix opérationnel et la télémétrie de cette exécution gelée. Un contrat de séquence admis est immutable jusqu'à libération ; seuls la séquence suivante et son lot peuvent être adaptés. Admission CPU et admission de lot sont indépendantes : CPU12 avec un lot de 1 est valide et ne permet pas d'emprunter une paire à la séquence suivante. Sous un signal de pression qui autorise encore une admission, la capacité CPU réductible abandonne l'essai et réserve CPU1/lot minimum dans cette même décision ; un seuil Gate G qui produit WAIT reste sans réservation.

Project DB v12 ajoute un enfant Geometric Verification Result référencé par match_result_id. Le Matcher ne calcule, ne stocke et ne valide aucun inlier géométrique ; seul son Match File canonique définit l'ordre indexé par le masque.

Backend Vulkan ORB

La frontière évaluée remplace uniquement KNN Hamming par un compute top-2 : un thread GPU par feature A parcourt B, conserve deux indices/distances et applique le tie-break du plus petit index. Elle ne matérialise jamais A×B. Lowe, canonicalisation et persistance restent communs. Sur Radeon 780M, la parité top-2 avec OpenCV est exacte et le gain warm est supérieur à 90 % à 4096/8192. Le backend de production conserve exactement cette frontière. Sa parité entière permet au CPU et à Vulkan de partager l'identité persistante. L'évidence directe de la tranche v2 établit ORB Vulkan comme déterministe, exact et mesurément supérieur pour ce hot path : il est le workload GPU primaire de la politique canonique GPU-first. Le CPU reste le fallback portable et de panne.

Les API normales créent AUTO. Pour ORB, elles exposent Vulkan depuis les faits build, objet backend, GPU et enveloppe UMA sûre ; création et reprise ne sondent ni n'initialisent le driver sur le caller/main. Le Governor essaie cette capacité avant le CPU complet, puis le premier begin initialise Vulkan sur le worker Queue déjà contraint. Avant ce worker, le démarrage a établi la politique sûre MESA_SHADER_CACHE_DISABLE=true; elle supprime sur la 780M validée les helpers de cache Mesa qui élargissaient leur masque. Une paire sous le seuil n'initialise toujours pas Vulkan. Aucun sweep/latch Matcher ni appel sched_*affinity(tid) auxiliaire n'existe ; le creator/main/TUI reste inchangé. La frontière d'initialisation du backend vérifie elle-même, sans setenv, que MESA_SHADER_CACHE_DISABLE vaut exactement true ou 1 avant tout appel Mesa. Un consumer public tardif avec une valeur absente, fausse ou malformée reçoit UNAVAILABLE, contexte mémorisé indisponible et aucune sortie partielle ; la metadata non initialisante reste lisible. Les variantes *_with_mode demandent encore explicitement CPU ou ORB Vulkan et conservent leurs erreurs historiques. Le mode Vulkan demande CPU1, GPU1 et 640 Kio GPU/UMA, débités exactement une fois de la RAM hôte sur la 780M. Un échec Vulkan pendant cette séquence reprend la paire top-2 CPU complète sans changer fingerprint ni résultat. Une panne d'initialisation retire Vulkan des admissions AUTO suivantes ; une paire sous le seuil validé reste seulement inéligible. Le rolling conserve explicitement l'état de soumission de chaque paire : une paire sans handle exécute directement son stage CPU complet et n'appelle jamais finish(NULL). Toute panne backend est publiée au Governor dès son observation, avant le fallback, l'annulation ou la publication qui peuvent encore échouer ; l'inéligibilité locale ne modifie jamais cette santé partagée et n'est jamais réétiquetée par l'échec d'une soumission ou d'une finition voisine. Le contrat détaillé est décrit dans vulkan_matcher.md. Les coutures privées renvoient séparément le résultat Matcher et la faute backend. Lecture/allocation locale avant submit, puis filtrage/allocation ou staging Match File après un finish réussi, produisent un fallback CPU complet classé other. Elles ne désactivent pas Vulkan et ne jettent pas un successeur soumis sain. Seul l'échec de la transaction backend exacte produit backend-failure et invalide les slots partagés.

La production normale est AUTO, choisi par l'unique Governor avant chaque séquence depuis une enveloppe privée CPU/Vulkan. Les modes explicites CPU/Vulkan sont des overrides de debug, benchmark et reproductibilité. Le contrat installé est immutable jusqu'au prochain sequence_break. Cette couture et ses dimensions retenues sont PASS / FROZEN.

Le CPU Matcher compte ses participants uniquement dans cpu_threads=1..12; helpers=0 tant qu'aucun helper GPU distinct n'est réellement admis. Le diagnostic borné conserve le backend de capacité sélectionné et le backend réel de la séquence (CPU, ORB_VULKAN ou plusieurs paires complètes des deux), avec la raison d'inéligibilité ou de fallback. Ce retour opérationnel ne change jamais l'identité ou le contenu scientifique.

La couture backend privée expose un snapshot cumulatif thread-safe : soumissions, complétions, temps CPU de submit/dispatch, attente de fence, readback, temps GPU issu des timestamps lorsqu'ils existent, intervalle entre une complétion observée et la soumission suivante, pannes, discards et nombre de slots actifs. Les compteurs saturent à UINT64_MAX au lieu de boucler. Matcher en dérive par séquence les soumissions/complétions, temps GPU/fence/starvation, travail CPU de fallback ou de staging et durée de publication durable. Ces chronométrages n'ajoutent ni worker, ni attente artificielle, ni changement d'ordre. Une séquence réellement fallback ne forme pas un échantillon de débit Vulkan pur.

Une nouvelle Task ORB normale persiste une signature MIXED avec les champs CPU12/GPU0 réels ; elle exprime honnêtement que la politique AUTO peut choisir CPU ou Vulkan par séquence, sans réserver les deux et sans coder un backend factice. L'override CPU conserve la classe CPU. La forme Vulkan explicite est CPU1/GPU1/640 Kio. Toutes sont à lot 1..12 et 10 Mio par paire. Les signatures historiques CPU8/GPU0 et CPU1/GPU1 à lot maximal 8, ainsi que les formes CPU12 pré-estimation-par-paire, restent reconnues uniquement pour la reprise et sont normalisées éphémèrement sans checkpoint d'estimation seule. Une forme voisine est rejetée ; aucun champ isolé ne sert à deviner le backend. Seule la nouvelle classe MIXED reconstruit AUTO. Toute forme CPU ORB historique ou courante reconstruit CPU fixe par compatibilité/sûreté des overrides ; Vulkan reste fixe. Le build portable conserve la signature AUTO mais son enveloppe n'expose que CPU. Seule une reconstruction MIXED/AUTO établit la disponibilité runtime partagée ; la co-restauration ultérieure de formes CPU, Vulkan ou historiques fixes ne la modifie pas. L'ordre de restauration ne peut donc pas déclasser une AUTO avant son admission.

L'évolution Compute Governor v2 n'ajoute aucune dimension de ressources au fingerprint. begin/finish/discard sont privés à src/orb_vulkan_backend_internal.h et absents du header/ABI public. begin transfère au backend les comptes exacts de la requête et un handle slot+generation; finish utilise ces comptes, borne FEATURE_MAX et ne consomme que ce slot sur tout résultat, même une capacité de sortie invalide. discard attend sa fence ; une attente en échec condamne/détruit la session avant une soumission suivante. Le wrapper public synchrone top-2 reste stable et s'implémente par cette couture. Les objets pending, fds et temporaires utilisent un nettoyage structuré, les chemins tronqués sont refusés et aucune exception C++ ne franchit C.

Le propriétaire Queue conserve seul Lowe, canonicalisation, publication et avancement du curseur. Device/pipeline/layout/cache sont partagés; deux slots maximum dupliquent seulement command/fence/descriptors/buffers/query. La trace déterministe prouve à depth 2, pour deux paires successives 769×769, GPU_SUBMIT(i) < GPU_SUBMIT(i+1) < GPU_FINISH(i) < PUBLICATION_START(i) < PUBLICATION_FINISH(i), avec cardinalité/sortie intactes.

Le payload mappé suit le contrat de séquence plutôt que le maximum de l'enveloppe. Un backend frais retient zéro, depth 1 retient exactement 640 Kio et depth 2 retient 1,25 Mio jusqu'au nettoyage de la séquence; le second slot est libéré avant la prochaine admission depth 1. Le redimensionnement est interdit avec une requête pending et une croissance échouée conserve la capacité antérieure. backend_info rapporte la rétention réelle. Une génération arrivée à UINT64_MAX retire définitivement son slot avant tout nouveau submit; elle ne boucle jamais vers un ancien handle. Elle couvre aussi échec du begin successeur, faute locale avant/après backend, échec de publication, annulation et réutilisation du slot. Le lifecycle est ORB_VULKAN_ASYNC_EXECUTION — PASS / FROZEN et helpers reste 0.

Le runner réel opt-in utilise désormais AUTO par défaut ; cpu et vulkan restent des overrides explicites de debug/benchmark/reproductibilité. Son pipeline normal est le rolling Governor-owned inflight 1. Un contrôle synchronous force depth 1 et est compilé uniquement dans ce runner et le test Matcher dédié : pour une capacité GPU sélectionnée, chaque paire passe par le wrapper public top-2 synchrone, puis par la même canonicalisation et la même publication avant la paire suivante. Ce contrôle historique de mesure n'existe pas dans le binaire lardon3d, ne répond à aucun environnement en production et ne devient ni identité, ni checkpoint, ni capacité Governor. Il est refusé pour CPU explicite et pour la reprise d'une Task Matcher pendante, car son choix n'est pas persisté. Les fixtures Vulkan comparent rolling depth 1 forcé, rolling depth 2 forcé et synchrone depth 1 sur des Projects neufs et obtiennent les mêmes champs scientifiques, SHA et tailles d'assets.

Pour isoler la profondeur du reste de la boucle adaptative, ce même runner accepte en contexte --resume-pre-gv-existing neuf le contrôle privé --matcher-inflight 1|2. Il est limité à AUTO; rolling force une capacité Vulkan unique et fixe min=max à la valeur demandée, avec un lot fixe de deux par défaut. Le contrôle additionnel --matcher-batch 2|4|8|12 exige inflight, AUTO et rolling, puis fixe aussi min=max batch pour une matrice reproductible. Le Governor choisit et réserve toujours cette capacité depuis son snapshot courant, y compris 640 Kio par slot et la charge UMA exacte; le contrôle ne court-circuite aucune admission et n'expose aucune alternative CPU. GPU budget zéro est refusé avant accès au Project; une capacité GPU/backend/mémoire non admissible invalide l'expérience au lieu d'exécuter AUTO CPU. Synchronous reste depth 1 et refuse la valeur 2. La valeur est absente du binaire lardon3d, de l'ABI, du payload, du checkpoint et de l'identité scientifique; le runner la restaure dans l'environnement sur toute sortie, la refuse sur une Task pendante et l'émet comme 1, 2 ou null dans le JSON d'évidence; batch est pareillement émis comme 2, 4, 8, 12 ou null. L'agrégat exige uniquement des contrats sélectionnés Vulkan batch/depth demandés, réservation hôte et payload exacts, zéro panne, discard ou slot pending. Il accepte seulement les fallbacks CPU complets dus à l'inéligibilité locale. Matcher compte chaque item une seule fois, après sa publication CPU durable, dans les classes local-ineligible/backend-failure/other; une séquence peut en contenir plusieurs et le CPU sélectionné normalement n'entre dans aucune classe. Le commit par item est séparé du feedback de débit de fin de séquence : une annulation ou panne ultérieure conserve le préfixe durable, mais n'entraîne jamais la séquence avortée. La déduplication bornée est éphémère à la Task et n'ajoute aucun état de reprise. L'agrégat conserve aussi les comptes de séquences pour diagnostic, mais l'égalité inter-cohortes porte exclusivement sur les items localement inéligibles; tout item backend-failure/other donne experiment_valid=false. Une panne backend tardive peut conserver la preuve CPU déjà publiée, mais la Task de benchmark échoue après son checkpoint et la cohorte ne peut pas être déclarée réussie. La mesure forcée ABBA donne 54,661652238 paires/s à depth 1 et 55,797311953 à depth 2 (+2,077617 %), sous le deadband 5 %. Pour 4113 paires durables par run, le débit de cohorte est (2 * 4113 * 1e9) / somme(wall_ns), pas la moyenne des débits par run. Les walls bruts 75326831673/75162582080 et 73662096698/73764360098 ns donnent les moyennes 75,244706877/73,713228398 s. Fence vaut 6,0684/3,6776 s, starvation 54,4534/50,1465 s, publication 29,2582/30,0548 s, submit CPU 0,2655/0,3818 s, readback 0,0460/0,0873 s et GPU busy max 23/24 %. Chaque bras A/B garde le digest 7a9dbc38a23a600379167d55e24836b7acbb22eea25573e7440bdc9e4602b3b3, quatre séquences de fallback local par exécution et zéro panne/discard. Depth 2 reste donc validé pour la sûreté privée (DEPTH_MAX_VALIDATED_SAFETY=2) mais est REJECTED_WITH_MEASURED_REASON pour la politique normale (DEPTH_MAX_USEFUL=1). Le corpus établit par ailleurs +10,27 % pour rolling depth 1 face au contrôle synchrone, avec le même digest. La matrice de batch runner-only item-valide donne, pour batch 2/4/8/12, les walls A/B bruts 76150272845/75674818393, 61319835797/63138565155, 55148130595/54847191200 et 53446248173/53724786321 ns. La même formule de débit combiné donne 54,180767704, 66,094373197, 74,784998723 et 76,755814095 paires/s, soit +21,988624373 %, +13,148812987 % puis +2,635308425 %. Chaque run conserve 4113 paires durables, six items locaux, zéro item backend-failure/other et le même digest ci-dessus. Batch 12 est donc REJECTED_WITH_MEASURED_REASON sous le deadband 5 % : la sûreté privée reste BATCH_MAX_VALIDATED_SAFETY=12, mais AUTO normal suit BATCH_MAX_USEFUL=8. Les fichiers sont forced-batch{2,4,8,12}-items{,-b}.stdout.jsonl. Les anciens forced-batch2-current.stdout.jsonl et forced-batch4.stdout.jsonl restent historiques : leur comparateur par séquences était invalide et ne fonde pas la décision. Le run normal sans option backend/lot/inflight short-auto-batch8-governor-v2.stdout.jsonl suit effectivement 1 → 2 → 4 → 8, conserve batch 8 et publie 4113/4113 résultats en 54,066973393 s (76,072 paires/s). Il garde le même digest L3DMRD1, six items locaux, zéro panne/discard, inflight 1 et helpers 0. Ce run est la preuve du contrôle AUTO; les cohortes forcées ci-dessus restent la preuve de sélection de l'enveloppe utile. Le run S21 final normal final-s21-auto.stdout.jsonl publie 172 741 Match Results pour 172 741 Candidate Pairs en 2 345,444485079 s, avec zéro mapping dupliqué, curseur contigu/complet et digest L3DMRD1 e5128a2e599ff593c4f79850e067254b1f249d19e8480a44973306b1af250f70. AUTO reste Vulkan/rolling; 172 507 soumissions ont 172 507 complétions, zéro panne/discard/pending, tandis que 234 items localement inéligibles sont recalculés comme paires CPU complètes. L'agrégat compte 21 550 séquences Vulkan pures, 77 mixtes et 3 CPU, sans jamais publier d'évidence partielle. La Task 2831 finit COMPLETE, progression 100 et sequence_count=21629; le checkpoint final SHA-256 vaut 636f4f4a20f27308d90142c495c9f6ffc04b4c0dfcca0fdc75cfeb5366ab50b1. Les quatre sources retenues sous /home/fy59/Documents/Lardon/.real-pre-sfm-2026-08-30/governor-v2-evidence/ sont forced-depth1-a.stdout.jsonl, forced-depth1-b.stdout.jsonl, forced-depth2-a.stdout.jsonl et forced-depth2-b.stdout.jsonl.

AUTO Vulkan normal adapte le lot seulement dans 1..8. Il exige huit observations pures consécutives pour la référence et huit au palier d'essai, puis accepte seulement un gain moyen d'au moins 5 %. Pression, fallback ou travail durable nul abandonnent la fenêtre; un ou deux échantillons ne décident rien. La durée entraînant AUTO couvre la réadmission réussie, le calcul, les publications ordonnées et le checkpoint générique durable; la seule durée du noyau Matcher reste diagnostique et ne décide pas le lot. Une séquence dont le checkpoint échoue ne peut donc pas entraîner le contrat suivant. CPU Matcher garde sa rampe et son maximum 12. Helpers reste 0 : la publication owner-only vaut environ 29,5 s dans ces cohortes, et ni le gain depth 2 sous 5 % ni une autre mesure ne prouve qu'un helper supplémentaire surmonterait cette frontière de durabilité ordonnée.

Le même runner audite la bijection ordonnée Candidate Pair/Match Result, le curseur final contigu et chaque Match File par SHA, taille, header et entrées. Son digest L3DMRD1 sérialise explicitement en largeurs fixes les IDs scientifiques, kind/version/fingerprint, status/cardinalité et SHA/taille d'asset ; Task IDs, timestamps, chemins et sélection opérationnelle sont exclus. Ce digest est une preuve de comparaison du harness, pas une nouvelle identité Project DB ou scientifique.

Déterminisme et fingerprint

Le fingerprint couvre kind, version, k=2, threshold float32 et absence de cross-check. À Feature Files, configuration, implémentation/version OpenCV et environnement numérique compatibles identiques, SIFT/RootSIFT garantissent les mêmes paires attendues, l'ordre et la sérialisation stables. Aucune promesse bit-à-bit cross-platform n'est faite pour les distances L2 flottantes. ORB/Hamming bénéficie d'une garantie plus forte grâce à sa distance entière.

NEXT: GEOMETRIC VERIFIER v3